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
magic_hour/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Root Client
2
+
3
+ <!-- CUSTOM DOCS START -->
4
+
5
+ <!-- CUSTOM DOCS END -->
6
+
7
+ ## Submodules
8
+ - [ai_clothes_changer](resources/v1/ai_clothes_changer/README.md) - ai_clothes_changer
9
+ - [ai_face_editor](resources/v1/ai_face_editor/README.md) - ai_face_editor
10
+ - [ai_gif_generator](resources/v1/ai_gif_generator/README.md) - ai_gif_generator
11
+ - [ai_headshot_generator](resources/v1/ai_headshot_generator/README.md) - ai_headshot_generator
12
+ - [ai_image_editor](resources/v1/ai_image_editor/README.md) - ai_image_editor
13
+ - [ai_image_generator](resources/v1/ai_image_generator/README.md) - ai_image_generator
14
+ - [ai_image_upscaler](resources/v1/ai_image_upscaler/README.md) - ai_image_upscaler
15
+ - [ai_meme_generator](resources/v1/ai_meme_generator/README.md) - ai_meme_generator
16
+ - [ai_photo_editor](resources/v1/ai_photo_editor/README.md) - ai_photo_editor
17
+ - [ai_qr_code_generator](resources/v1/ai_qr_code_generator/README.md) - ai_qr_code_generator
18
+ - [ai_talking_photo](resources/v1/ai_talking_photo/README.md) - ai_talking_photo
19
+ - [animation](resources/v1/animation/README.md) - animation
20
+ - [auto_subtitle_generator](resources/v1/auto_subtitle_generator/README.md) - auto_subtitle_generator
21
+ - [face_detection](resources/v1/face_detection/README.md) - face_detection
22
+ - [face_swap](resources/v1/face_swap/README.md) - face_swap
23
+ - [face_swap_photo](resources/v1/face_swap_photo/README.md) - face_swap_photo
24
+ - [upload_urls](resources/v1/files/upload_urls/README.md) - upload_urls
25
+ - [files](resources/v1/files/README.md) - files
26
+ - [image_background_remover](resources/v1/image_background_remover/README.md) - image_background_remover
27
+ - [image_projects](resources/v1/image_projects/README.md) - image_projects
28
+ - [image_to_video](resources/v1/image_to_video/README.md) - image_to_video
29
+ - [lip_sync](resources/v1/lip_sync/README.md) - lip_sync
30
+ - [photo_colorizer](resources/v1/photo_colorizer/README.md) - photo_colorizer
31
+ - [text_to_video](resources/v1/text_to_video/README.md) - text_to_video
32
+ - [video_projects](resources/v1/video_projects/README.md) - video_projects
33
+ - [video_to_video](resources/v1/video_to_video/README.md) - video_to_video
34
+ - [v1](resources/v1/README.md) - v1
35
+
@@ -15,7 +15,8 @@ from pydantic import BaseModel
15
15
 
16
16
  from .api_error import ApiError
17
17
  from .auth import AuthProvider
18
- from .request import RequestConfig, RequestOptions, default_request_options, QueryParams
18
+ from .request import RequestConfig, RequestOptions, default_request_options
19
+ from .query import QueryParams
19
20
  from .response import from_encodable, AsyncStreamResponse, StreamResponse
20
21
  from .utils import get_response_type, filter_binary_response
21
22
  from .binary_response import BinaryResponse
@@ -51,7 +52,7 @@ class BaseClient:
51
52
  )
52
53
  self._auths: Dict[str, AuthProvider] = auths or {}
53
54
 
54
- def register_auth(self, auth_id: str, provider: AuthProvider):
55
+ def register_auth(self, auth_id: str, provider: AuthProvider) -> None:
55
56
  """Register an authentication provider.
56
57
 
57
58
  Args:
@@ -297,7 +298,7 @@ class BaseClient:
297
298
  def process_response(
298
299
  self,
299
300
  *,
300
- response=httpx.Response,
301
+ response: httpx.Response,
301
302
  cast_to: Union[Type[T], Any],
302
303
  ) -> T:
303
304
  """Process an HTTP response and convert it to the desired type.
@@ -325,8 +326,8 @@ class BaseClient:
325
326
 
326
327
  if response_type == "json":
327
328
  if cast_to is type(Any):
328
- return response.json()
329
- return from_encodable(
329
+ return response.json() # type: ignore[no-any-return]
330
+ return from_encodable( # type: ignore[no-any-return]
330
331
  data=response.json(), load_with=filter_binary_response(cast_to=cast_to)
331
332
  )
332
333
  elif response_type == "text":
magic_hour/core/query.py CHANGED
@@ -19,7 +19,7 @@ def encode_query_param(
19
19
  value: Any,
20
20
  style: QueryParamStyle = "form",
21
21
  explode: bool = True,
22
- ):
22
+ ) -> None:
23
23
  if style == "form":
24
24
  _encode_form(params, name, value, explode)
25
25
  elif style == "spaceDelimited":
@@ -39,7 +39,7 @@ def _query_str(val: Any) -> str:
39
39
  return json.dumps(val)
40
40
 
41
41
 
42
- def _encode_form(params: QueryParams, name: str, value: Any, explode: bool):
42
+ def _encode_form(params: QueryParams, name: str, value: Any, explode: bool) -> None:
43
43
  """
44
44
  Encodes query params in the `form` style as defined by OpenAPI with both explode and non-explode
45
45
  variants.
@@ -63,7 +63,9 @@ def _encode_form(params: QueryParams, name: str, value: Any, explode: bool):
63
63
  params[name] = value
64
64
 
65
65
 
66
- def _encode_spaced_delimited(params: QueryParams, name: str, value: Any, explode: bool):
66
+ def _encode_spaced_delimited(
67
+ params: QueryParams, name: str, value: Any, explode: bool
68
+ ) -> None:
67
69
  """
68
70
  Encodes query params in the `spaceDelimited` style as defined by OpenAPI with both explode and non-explode
69
71
  variants.
@@ -78,7 +80,9 @@ def _encode_spaced_delimited(params: QueryParams, name: str, value: Any, explode
78
80
  _encode_form(params, name, value, explode)
79
81
 
80
82
 
81
- def _encode_pipe_delimited(params: QueryParams, name: str, value: Any, explode: bool):
83
+ def _encode_pipe_delimited(
84
+ params: QueryParams, name: str, value: Any, explode: bool
85
+ ) -> None:
82
86
  """
83
87
  Encodes query params in the `pipeDelimited` style as defined by OpenAPI with both explode and non-explode
84
88
  variants.
@@ -93,7 +97,9 @@ def _encode_pipe_delimited(params: QueryParams, name: str, value: Any, explode:
93
97
  _encode_form(params, name, value, explode)
94
98
 
95
99
 
96
- def _encode_deep_object(params: QueryParams, name: str, value: Any, explode: bool):
100
+ def _encode_deep_object(
101
+ params: QueryParams, name: str, value: Any, explode: bool
102
+ ) -> None:
97
103
  """
98
104
  Encodes query params in the `deepObject` style as defined by with both explode and non-explode
99
105
  variants.
@@ -107,7 +113,7 @@ def _encode_deep_object(params: QueryParams, name: str, value: Any, explode: boo
107
113
  _encode_form(params, name, value, explode)
108
114
 
109
115
 
110
- def _encode_deep_object_key(params: QueryParams, key: str, value: Any):
116
+ def _encode_deep_object_key(params: QueryParams, key: str, value: Any) -> None:
111
117
  if isinstance(value, dict):
112
118
  for k, v in value.items():
113
119
  _encode_deep_object_key(params, f"{key}[{k}]", v)
@@ -82,7 +82,7 @@ def model_dump(item: Any) -> Any:
82
82
 
83
83
 
84
84
  def to_encodable(
85
- *, item: Any, dump_with: Union[Type, Union[Type, Any], List[Type]]
85
+ *, item: Any, dump_with: Union[Type[Any], Union[Type[Any], Any], List[Type[Any]]]
86
86
  ) -> Any:
87
87
  """
88
88
  Validates and converts an item to an encodable format using a specified type.
@@ -90,7 +90,7 @@ def to_encodable(
90
90
  to a format suitable for encoding in requests.
91
91
  """
92
92
  filtered_item = filter_not_given(item)
93
- adapter: TypeAdapter = TypeAdapter(dump_with)
93
+ adapter: TypeAdapter[Any] = TypeAdapter(dump_with)
94
94
  validated_item = adapter.validate_python(filtered_item)
95
95
  return model_dump(validated_item)
96
96
 
@@ -98,7 +98,7 @@ def to_encodable(
98
98
  def to_form_urlencoded(
99
99
  *,
100
100
  item: Any,
101
- dump_with: Union[Type, Union[Type, Any]],
101
+ dump_with: Union[Type[Any], Union[Type[Any], Any]],
102
102
  style: Mapping[str, QueryParamStyle],
103
103
  explode: Mapping[str, bool],
104
104
  ) -> Mapping[str, Any]:
@@ -49,7 +49,9 @@ class StreamResponse(Generic[T]):
49
49
  into the specified type.
50
50
  """
51
51
 
52
- def __init__(self, response: httpx.Response, stream_context, cast_to: Type[T]):
52
+ def __init__(
53
+ self, response: httpx.Response, stream_context: Any, cast_to: Type[T]
54
+ ) -> None:
53
55
  """
54
56
  Initialize the stream processor with response and conversion settings.
55
57
 
@@ -65,7 +67,7 @@ class StreamResponse(Generic[T]):
65
67
  self.buffer = bytearray()
66
68
  self.position = 0
67
69
 
68
- def __iter__(self):
70
+ def __iter__(self) -> "StreamResponse[T]":
69
71
  """Enables iteration over the stream events."""
70
72
  return self
71
73
 
@@ -95,7 +97,7 @@ class StreamResponse(Generic[T]):
95
97
  self._context.__exit__(None, None, None)
96
98
  raise
97
99
 
98
- def _process_buffer(self, final=False) -> Optional[T]:
100
+ def _process_buffer(self, final: bool = False) -> Optional[T]:
99
101
  """
100
102
  Processes the current buffer to extract complete SSE events.
101
103
 
@@ -125,11 +127,11 @@ class StreamResponse(Generic[T]):
125
127
  or "data" not in parsed_data
126
128
  ):
127
129
  parsed_data = {"data": parsed_data}
128
- return from_encodable(
130
+ return from_encodable( # type: ignore[no-any-return]
129
131
  data=parsed_data, load_with=self.cast_to
130
132
  )
131
133
  except json.JSONDecodeError:
132
- return from_encodable(
134
+ return from_encodable( # type: ignore[no-any-return]
133
135
  data={"data": data}, load_with=self.cast_to
134
136
  )
135
137
  return None
@@ -144,9 +146,9 @@ class StreamResponse(Generic[T]):
144
146
  parsed_data = json.loads(data)
145
147
  if not isinstance(parsed_data, dict) or "data" not in parsed_data:
146
148
  parsed_data = {"data": parsed_data}
147
- return from_encodable(data=parsed_data, load_with=self.cast_to)
149
+ return from_encodable(data=parsed_data, load_with=self.cast_to) # type: ignore[no-any-return]
148
150
  except json.JSONDecodeError:
149
- return from_encodable(data={"data": data}, load_with=self.cast_to)
151
+ return from_encodable(data={"data": data}, load_with=self.cast_to) # type: ignore[no-any-return]
150
152
 
151
153
  return None
152
154
 
@@ -177,7 +179,9 @@ class AsyncStreamResponse(Generic[T]):
177
179
  but compatible with async/await syntax.
178
180
  """
179
181
 
180
- def __init__(self, response: httpx.Response, stream_context, cast_to: Type[T]):
182
+ def __init__(
183
+ self, response: httpx.Response, stream_context: Any, cast_to: Type[T]
184
+ ) -> None:
181
185
  """
182
186
  Initialize the async stream processor.
183
187
 
@@ -193,7 +197,7 @@ class AsyncStreamResponse(Generic[T]):
193
197
  self.buffer = bytearray()
194
198
  self.position = 0
195
199
 
196
- def __aiter__(self):
200
+ def __aiter__(self) -> "AsyncStreamResponse[T]":
197
201
  """Enables async iteration over the stream events."""
198
202
  return self
199
203
 
@@ -223,7 +227,7 @@ class AsyncStreamResponse(Generic[T]):
223
227
  await self._context.__aexit__(None, None, None)
224
228
  raise
225
229
 
226
- def _process_buffer(self, final=False) -> Optional[T]:
230
+ def _process_buffer(self, final: bool = False) -> Optional[T]:
227
231
  """
228
232
  Processes the current buffer to extract complete SSE events.
229
233
 
@@ -249,11 +253,11 @@ class AsyncStreamResponse(Generic[T]):
249
253
  or "data" not in parsed_data
250
254
  ):
251
255
  parsed_data = {"data": parsed_data}
252
- return from_encodable(
256
+ return from_encodable( # type: ignore[no-any-return]
253
257
  data=parsed_data, load_with=self.cast_to
254
258
  )
255
259
  except json.JSONDecodeError:
256
- return from_encodable(
260
+ return from_encodable( # type: ignore[no-any-return]
257
261
  data={"data": data}, load_with=self.cast_to
258
262
  )
259
263
  return None
@@ -268,9 +272,9 @@ class AsyncStreamResponse(Generic[T]):
268
272
  parsed_data = json.loads(data)
269
273
  if not isinstance(parsed_data, dict) or "data" not in parsed_data:
270
274
  parsed_data = {"data": parsed_data}
271
- return from_encodable(data=parsed_data, load_with=self.cast_to)
275
+ return from_encodable(data=parsed_data, load_with=self.cast_to) # type: ignore[no-any-return]
272
276
  except json.JSONDecodeError:
273
- return from_encodable(data={"data": data}, load_with=self.cast_to)
277
+ return from_encodable(data={"data": data}, load_with=self.cast_to) # type: ignore[no-any-return]
274
278
 
275
279
  return None
276
280
 
magic_hour/core/utils.py CHANGED
@@ -34,7 +34,7 @@ def is_union_type(type_hint: typing.Any) -> bool:
34
34
  return hasattr(type_hint, "__origin__") and type_hint.__origin__ is typing.Union
35
35
 
36
36
 
37
- def filter_binary_response(cast_to: typing.Type) -> typing.Type:
37
+ def filter_binary_response(cast_to: typing.Type[typing.Any]) -> typing.Type[typing.Any]:
38
38
  """
39
39
  Filters out BinaryResponse from a Union type.
40
40
  If cast_to is not a Union, returns it unchanged.
@@ -50,6 +50,6 @@ def filter_binary_response(cast_to: typing.Type) -> typing.Type:
50
50
  return cast_to
51
51
  # If only one type remains, return it directly
52
52
  if len(filtered) == 1:
53
- return typing.cast(typing.Type, filtered[0])
53
+ return typing.cast(typing.Type[typing.Any], filtered[0])
54
54
  # Otherwise return new Union with filtered types
55
- return typing.cast(typing.Type, typing.Union[filtered]) # type: ignore
55
+ return typing.cast(typing.Type[typing.Any], typing.Union[filtered])
magic_hour/environment.py CHANGED
@@ -6,7 +6,7 @@ class Environment(enum.Enum):
6
6
  """Pre-defined base URLs for the API"""
7
7
 
8
8
  ENVIRONMENT = "https://api.magichour.ai"
9
- MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.35.0"
9
+ MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.36.0"
10
10
 
11
11
 
12
12
  def _get_base_url(
@@ -0,0 +1,3 @@
1
+ from .download import download_files_sync, download_files_async
2
+
3
+ __all__ = ["download_files_sync", "download_files_async"]
@@ -0,0 +1,75 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Union, List
4
+ from urllib.parse import urlparse
5
+ import httpx
6
+ from magic_hour.types import models
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _compute_download_path(
13
+ url: str, download_directory: Union[str, None] = None
14
+ ) -> str:
15
+ url_path = urlparse(url).path
16
+ filename = Path(url_path).name
17
+ if download_directory:
18
+ return os.path.join(download_directory, filename)
19
+ return filename
20
+
21
+
22
+ def download_files_sync(
23
+ downloads: Union[
24
+ List[models.V1ImageProjectsGetResponseDownloadsItem],
25
+ List[models.V1VideoProjectsGetResponseDownloadsItem],
26
+ ],
27
+ download_directory: Union[str, None] = None,
28
+ ) -> List[str]:
29
+ downloaded_paths: List[str] = []
30
+
31
+ for download in downloads:
32
+ with httpx.Client() as http_client:
33
+ download_response = http_client.get(download.url)
34
+ download_response.raise_for_status()
35
+
36
+ download_path = _compute_download_path(
37
+ download.url, download_directory=download_directory
38
+ )
39
+
40
+ with open(download_path, "wb") as f:
41
+ f.write(download_response.content)
42
+
43
+ downloaded_paths.append(download_path)
44
+
45
+ logger.info(f"Downloaded file saved as: {download_path}")
46
+
47
+ return downloaded_paths
48
+
49
+
50
+ async def download_files_async(
51
+ downloads: Union[
52
+ List[models.V1ImageProjectsGetResponseDownloadsItem],
53
+ List[models.V1VideoProjectsGetResponseDownloadsItem],
54
+ ],
55
+ download_directory: Union[str, None] = None,
56
+ ) -> List[str]:
57
+ downloaded_paths: List[str] = []
58
+
59
+ for download in downloads:
60
+ async with httpx.AsyncClient() as http_client:
61
+ download_response = await http_client.get(download.url)
62
+ download_response.raise_for_status()
63
+
64
+ download_path = _compute_download_path(
65
+ download.url, download_directory=download_directory
66
+ )
67
+
68
+ with open(download_path, "wb") as f:
69
+ f.write(download_response.content)
70
+
71
+ downloaded_paths.append(download_path)
72
+
73
+ logger.info(f"Downloaded file saved as: {download_path}")
74
+
75
+ return downloaded_paths
@@ -0,0 +1,33 @@
1
+ # v1
2
+
3
+ <!-- CUSTOM DOCS START -->
4
+
5
+ <!-- CUSTOM DOCS END -->
6
+
7
+ ## Submodules
8
+ - [ai_clothes_changer](ai_clothes_changer/README.md) - ai_clothes_changer
9
+ - [ai_face_editor](ai_face_editor/README.md) - ai_face_editor
10
+ - [ai_gif_generator](ai_gif_generator/README.md) - ai_gif_generator
11
+ - [ai_headshot_generator](ai_headshot_generator/README.md) - ai_headshot_generator
12
+ - [ai_image_editor](ai_image_editor/README.md) - ai_image_editor
13
+ - [ai_image_generator](ai_image_generator/README.md) - ai_image_generator
14
+ - [ai_image_upscaler](ai_image_upscaler/README.md) - ai_image_upscaler
15
+ - [ai_meme_generator](ai_meme_generator/README.md) - ai_meme_generator
16
+ - [ai_photo_editor](ai_photo_editor/README.md) - ai_photo_editor
17
+ - [ai_qr_code_generator](ai_qr_code_generator/README.md) - ai_qr_code_generator
18
+ - [ai_talking_photo](ai_talking_photo/README.md) - ai_talking_photo
19
+ - [animation](animation/README.md) - animation
20
+ - [auto_subtitle_generator](auto_subtitle_generator/README.md) - auto_subtitle_generator
21
+ - [face_detection](face_detection/README.md) - face_detection
22
+ - [face_swap](face_swap/README.md) - face_swap
23
+ - [face_swap_photo](face_swap_photo/README.md) - face_swap_photo
24
+ - [files](files/README.md) - files
25
+ - [image_background_remover](image_background_remover/README.md) - image_background_remover
26
+ - [image_projects](image_projects/README.md) - image_projects
27
+ - [image_to_video](image_to_video/README.md) - image_to_video
28
+ - [lip_sync](lip_sync/README.md) - lip_sync
29
+ - [photo_colorizer](photo_colorizer/README.md) - photo_colorizer
30
+ - [text_to_video](text_to_video/README.md) - text_to_video
31
+ - [video_projects](video_projects/README.md) - video_projects
32
+ - [video_to_video](video_to_video/README.md) - video_to_video
33
+
@@ -1,3 +1,72 @@
1
+ # v1_ai_clothes_changer
2
+
3
+ ## Module Functions
4
+
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ ### AI Clothes Changer 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.ai_clothes_changer.generate(
35
+ assets={
36
+ "garment_file_path": "/path/to/outfit.png",
37
+ "garment_type": "upper_body",
38
+ "person_file_path": "/path/to/model.png",
39
+ },
40
+ name="Clothes Changer image",
41
+ wait_for_completion=True,
42
+ download_outputs=True,
43
+ download_directory="outputs",
44
+ )
45
+ ```
46
+
47
+ #### Asynchronous Client
48
+
49
+ ```python
50
+ from magic_hour import AsyncClient
51
+ from os import getenv
52
+
53
+ client = AsyncClient(token=getenv("API_TOKEN"))
54
+ res = await client.v1.ai_clothes_changer.generate(
55
+ assets={
56
+ "garment_file_path": "/path/to/outfit.png",
57
+ "garment_type": "upper_body",
58
+ "person_file_path": "/path/to/model.png",
59
+ },
60
+ name="Clothes Changer image",
61
+ download_directory="outputs",
62
+ wait_for_completion=True,
63
+ download_outputs=True,
64
+ download_directory="outputs",
65
+ )
66
+ ```
67
+
68
+ <!-- CUSTOM DOCS END -->
69
+
1
70
 
2
71
  ### AI Clothes Changer <a name="create"></a>
3
72
 
@@ -10,6 +79,9 @@ Change outfits in photos in seconds with just a photo reference. Each photo cost
10
79
  | Parameter | Required | Description | Example |
11
80
  |-----------|:--------:|-------------|--------|
12
81
  | `assets` | ✓ | Provide the assets for clothes changer | `{"garment_file_path": "api-assets/id/outfit.png", "garment_type": "upper_body", "person_file_path": "api-assets/id/model.png"}` |
82
+ | `└─ garment_file_path` | ✓ | The image of the outfit. 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/outfit.png"` |
83
+ | `└─ garment_type` | ✓ | The type of the outfit. | `"upper_body"` |
84
+ | `└─ person_file_path` | ✓ | The image with the person. 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/model.png"` |
13
85
  | `name` | ✗ | The name of image. This value is mainly used for your own identification of the image. | `"Clothes Changer image"` |
14
86
 
15
87
  #### Synchronous Client
@@ -55,3 +127,4 @@ res = await client.v1.ai_clothes_changer.create(
55
127
 
56
128
  ##### Example
57
129
  `{"credits_charged": 25, "frame_cost": 25, "id": "cuid-example"}`
130
+
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import typing
2
3
 
3
4
  from magic_hour.core import (
@@ -8,13 +9,88 @@ 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 AiClothesChangerClient:
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.V1AiClothesChangerGenerateBodyAssets,
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 clothes changed image (alias for create with additional functionality).
42
+
43
+ Change clothes in an image using AI. Each change 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 clothes changer
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 AI Clothes Changer API with the downloaded paths if `download_outputs` is True.
55
+
56
+ Examples:
57
+ ```py
58
+ response = client.v1.ai_clothes_changer.generate(
59
+ assets={
60
+ "garment_file_path": "path/to/outfit.png",
61
+ "garment_type": "upper_body",
62
+ "person_file_path": "path/to/model.png",
63
+ },
64
+ name="Clothes Changer 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
+ garment_file_path = assets["garment_file_path"]
75
+ person_file_path = assets["person_file_path"]
76
+ assets["garment_file_path"] = file_client.upload_file(file=garment_file_path)
77
+ assets["person_file_path"] = file_client.upload_file(file=person_file_path)
78
+
79
+ create_response = self.create(
80
+ assets=assets, name=name, request_options=request_options
81
+ )
82
+ logger.info(f"AI Clothes Changer response: {create_response}")
83
+
84
+ image_projects_client = ImageProjectsClient(base_client=self._base_client)
85
+ response = image_projects_client.check_result(
86
+ id=create_response.id,
87
+ wait_for_completion=wait_for_completion,
88
+ download_outputs=download_outputs,
89
+ download_directory=download_directory,
90
+ )
91
+
92
+ return response
93
+
18
94
  def create(
19
95
  self,
20
96
  *,
@@ -73,6 +149,76 @@ class AsyncAiClothesChangerClient:
73
149
  def __init__(self, *, base_client: AsyncBaseClient):
74
150
  self._base_client = base_client
75
151
 
152
+ async def generate(
153
+ self,
154
+ *,
155
+ assets: params.V1AiClothesChangerGenerateBodyAssets,
156
+ name: typing.Union[
157
+ typing.Optional[str], type_utils.NotGiven
158
+ ] = type_utils.NOT_GIVEN,
159
+ wait_for_completion: bool = True,
160
+ download_outputs: bool = True,
161
+ download_directory: typing.Optional[str] = None,
162
+ request_options: typing.Optional[RequestOptions] = None,
163
+ ):
164
+ """
165
+ Generate clothes changed image (alias for create with additional functionality).
166
+
167
+ Change clothes in an image using AI. Each change costs 5 credits.
168
+
169
+ Args:
170
+ name: The name of image. This value is mainly used for your own identification of the image.
171
+ assets: Provide the assets for clothes changer
172
+ wait_for_completion: Whether to wait for the image project to complete
173
+ download_outputs: Whether to download the outputs
174
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
175
+ request_options: Additional options to customize the HTTP request
176
+
177
+ Returns:
178
+ V1ImageProjectsGetResponseWithDownloads: The response from the AI Clothes Changer API with the downloaded paths if `download_outputs` is True.
179
+
180
+ Examples:
181
+ ```py
182
+ response = await client.v1.ai_clothes_changer.generate(
183
+ assets={
184
+ "garment_file_path": "path/to/outfit.png",
185
+ "garment_type": "upper_body",
186
+ "person_file_path": "path/to/model.png",
187
+ },
188
+ name="Clothes Changer image",
189
+ wait_for_completion=True,
190
+ download_outputs=True,
191
+ download_directory="outputs/",
192
+ )
193
+ ```
194
+ """
195
+
196
+ file_client = AsyncFilesClient(base_client=self._base_client)
197
+
198
+ garment_file_path = assets["garment_file_path"]
199
+ person_file_path = assets["person_file_path"]
200
+ assets["garment_file_path"] = await file_client.upload_file(
201
+ file=garment_file_path
202
+ )
203
+ assets["person_file_path"] = await file_client.upload_file(
204
+ file=person_file_path
205
+ )
206
+
207
+ create_response = await self.create(
208
+ assets=assets, name=name, request_options=request_options
209
+ )
210
+ logger.info(f"AI Clothes Changer response: {create_response}")
211
+
212
+ image_projects_client = AsyncImageProjectsClient(base_client=self._base_client)
213
+ response = await image_projects_client.check_result(
214
+ id=create_response.id,
215
+ wait_for_completion=wait_for_completion,
216
+ download_outputs=download_outputs,
217
+ download_directory=download_directory,
218
+ )
219
+
220
+ return response
221
+
76
222
  async def create(
77
223
  self,
78
224
  *,