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.
- magic_hour/README.md +35 -0
- magic_hour/core/base_client.py +6 -5
- magic_hour/core/query.py +12 -6
- magic_hour/core/request.py +3 -3
- magic_hour/core/response.py +18 -14
- magic_hour/core/utils.py +3 -3
- magic_hour/environment.py +1 -1
- magic_hour/helpers/__init__.py +3 -0
- magic_hour/helpers/download.py +75 -0
- magic_hour/resources/v1/README.md +33 -0
- magic_hour/resources/v1/ai_clothes_changer/README.md +73 -0
- magic_hour/resources/v1/ai_clothes_changer/client.py +146 -0
- magic_hour/resources/v1/ai_face_editor/README.md +110 -0
- magic_hour/resources/v1/ai_face_editor/client.py +168 -0
- magic_hour/resources/v1/ai_gif_generator/README.md +59 -0
- magic_hour/resources/v1/ai_gif_generator/client.py +119 -0
- magic_hour/resources/v1/ai_headshot_generator/README.md +60 -0
- magic_hour/resources/v1/ai_headshot_generator/client.py +140 -0
- magic_hour/resources/v1/ai_image_editor/README.md +64 -0
- magic_hour/resources/v1/ai_image_editor/client.py +136 -0
- magic_hour/resources/v1/ai_image_generator/README.md +66 -0
- magic_hour/resources/v1/ai_image_generator/client.py +139 -0
- magic_hour/resources/v1/ai_image_upscaler/README.md +67 -0
- magic_hour/resources/v1/ai_image_upscaler/client.py +150 -0
- magic_hour/resources/v1/ai_meme_generator/README.md +71 -0
- magic_hour/resources/v1/ai_meme_generator/client.py +127 -0
- magic_hour/resources/v1/ai_photo_editor/README.md +98 -7
- magic_hour/resources/v1/ai_photo_editor/client.py +174 -0
- magic_hour/resources/v1/ai_qr_code_generator/README.md +63 -0
- magic_hour/resources/v1/ai_qr_code_generator/client.py +123 -0
- magic_hour/resources/v1/ai_talking_photo/README.md +74 -0
- magic_hour/resources/v1/ai_talking_photo/client.py +170 -0
- magic_hour/resources/v1/animation/README.md +100 -0
- magic_hour/resources/v1/animation/client.py +218 -0
- magic_hour/resources/v1/auto_subtitle_generator/README.md +69 -0
- magic_hour/resources/v1/auto_subtitle_generator/client.py +178 -0
- magic_hour/resources/v1/face_detection/README.md +59 -0
- magic_hour/resources/v1/face_detection/__init__.py +10 -2
- magic_hour/resources/v1/face_detection/client.py +179 -0
- magic_hour/resources/v1/face_swap/README.md +105 -8
- magic_hour/resources/v1/face_swap/client.py +242 -0
- magic_hour/resources/v1/face_swap_photo/README.md +84 -0
- magic_hour/resources/v1/face_swap_photo/client.py +172 -0
- magic_hour/resources/v1/files/README.md +40 -0
- magic_hour/resources/v1/files/client.py +350 -0
- magic_hour/resources/v1/files/client_test.py +414 -0
- magic_hour/resources/v1/files/upload_urls/README.md +8 -0
- magic_hour/resources/v1/image_background_remover/README.md +68 -0
- magic_hour/resources/v1/image_background_remover/client.py +130 -0
- magic_hour/resources/v1/image_projects/README.md +52 -0
- magic_hour/resources/v1/image_projects/__init__.py +10 -2
- magic_hour/resources/v1/image_projects/client.py +138 -0
- magic_hour/resources/v1/image_projects/client_test.py +527 -0
- magic_hour/resources/v1/image_to_video/README.md +77 -9
- magic_hour/resources/v1/image_to_video/client.py +186 -0
- magic_hour/resources/v1/lip_sync/README.md +87 -9
- magic_hour/resources/v1/lip_sync/client.py +210 -0
- magic_hour/resources/v1/photo_colorizer/README.md +59 -0
- magic_hour/resources/v1/photo_colorizer/client.py +130 -0
- magic_hour/resources/v1/text_to_video/README.md +68 -0
- magic_hour/resources/v1/text_to_video/client.py +151 -0
- magic_hour/resources/v1/video_projects/README.md +52 -0
- magic_hour/resources/v1/video_projects/__init__.py +10 -2
- magic_hour/resources/v1/video_projects/client.py +137 -0
- magic_hour/resources/v1/video_projects/client_test.py +527 -0
- magic_hour/resources/v1/video_to_video/README.md +98 -10
- magic_hour/resources/v1/video_to_video/client.py +222 -0
- magic_hour/types/params/__init__.py +58 -0
- magic_hour/types/params/v1_ai_clothes_changer_generate_body_assets.py +33 -0
- magic_hour/types/params/v1_ai_face_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_headshot_generator_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_image_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_image_upscaler_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_photo_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_talking_photo_generate_body_assets.py +26 -0
- magic_hour/types/params/v1_animation_generate_body_assets.py +39 -0
- magic_hour/types/params/v1_auto_subtitle_generator_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_face_detection_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_face_swap_create_body.py +12 -0
- magic_hour/types/params/v1_face_swap_create_body_style.py +33 -0
- magic_hour/types/params/v1_face_swap_generate_body_assets.py +56 -0
- magic_hour/types/params/v1_face_swap_generate_body_assets_face_mappings_item.py +25 -0
- magic_hour/types/params/v1_face_swap_photo_generate_body_assets.py +47 -0
- magic_hour/types/params/v1_face_swap_photo_generate_body_assets_face_mappings_item.py +25 -0
- magic_hour/types/params/v1_image_background_remover_generate_body_assets.py +27 -0
- magic_hour/types/params/v1_image_to_video_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
- magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
- magic_hour-0.36.1.dist-info/METADATA +306 -0
- {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/RECORD +93 -65
- magic_hour-0.35.0.dist-info/METADATA +0 -166
- {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/LICENSE +0 -0
- {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
|
+
|
magic_hour/core/base_client.py
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
magic_hour/core/request.py
CHANGED
|
@@ -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]:
|
magic_hour/core/response.py
CHANGED
|
@@ -49,7 +49,9 @@ class StreamResponse(Generic[T]):
|
|
|
49
49
|
into the specified type.
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
def __init__(
|
|
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__(
|
|
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])
|
|
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.
|
|
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,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
|
*,
|