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
@@ -0,0 +1,414 @@
1
+ import pytest
2
+ import tempfile
3
+ import os
4
+ import io
5
+ import pathlib
6
+ from unittest import mock
7
+
8
+ from magic_hour import AsyncClient, Client
9
+ from magic_hour.environment import Environment
10
+
11
+
12
+ def test_upload_file_local():
13
+ data = b"test data"
14
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
15
+ tmp.write(data)
16
+ tmp_path = tmp.name
17
+
18
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
19
+
20
+ with mock.patch("httpx.Client.put") as mock_put:
21
+ mock_put.return_value = mock.Mock(
22
+ status_code=200, raise_for_status=lambda: None
23
+ )
24
+ result = client.v1.files.upload_file(tmp_path)
25
+ assert result == "api-assets/id/video.mp4"
26
+ mock_put.assert_called_once_with(url=mock.ANY, content=data)
27
+
28
+ os.remove(tmp_path)
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_async_upload_file_local():
33
+ data = b"test data"
34
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
35
+ tmp.write(data)
36
+ tmp_path = tmp.name
37
+
38
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
39
+
40
+ with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
41
+ mock_put.return_value = mock.Mock(
42
+ status_code=200, raise_for_status=lambda: None
43
+ )
44
+ result = await client.v1.files.upload_file(tmp_path)
45
+ assert result == "api-assets/id/video.mp4"
46
+ mock_put.assert_awaited_once_with(url=mock.ANY, content=data)
47
+
48
+ os.remove(tmp_path)
49
+
50
+
51
+ def test_upload_file_with_binary_io():
52
+ data = b"test image data"
53
+ file_obj = io.BytesIO(data)
54
+ file_obj.name = "test.png" # Required for extension detection
55
+
56
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
57
+
58
+ with mock.patch("httpx.Client.put") as mock_put:
59
+ mock_put.return_value = mock.Mock(
60
+ status_code=200, raise_for_status=lambda: None
61
+ )
62
+ result = client.v1.files.upload_file(file_obj)
63
+ assert result == "api-assets/id/video.mp4"
64
+ mock_put.assert_called_once_with(
65
+ url=mock.ANY,
66
+ content=data,
67
+ )
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_async_upload_file_with_binary_io():
72
+ data = b"test audio data"
73
+ file_obj = io.BytesIO(data)
74
+ file_obj.name = "test.wav"
75
+
76
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
77
+
78
+ with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
79
+ mock_put.return_value = mock.Mock(
80
+ status_code=200, raise_for_status=lambda: None
81
+ )
82
+ result = await client.v1.files.upload_file(file_obj)
83
+ assert result == "api-assets/id/video.mp4"
84
+ mock_put.assert_awaited_once_with(url=mock.ANY, content=data)
85
+
86
+
87
+ # Test pathlib.Path input
88
+ def test_upload_file_with_pathlib_path():
89
+ data = b"test data"
90
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
91
+ tmp.write(data)
92
+ tmp_path = pathlib.Path(tmp.name)
93
+
94
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
95
+
96
+ with mock.patch("httpx.Client.put") as mock_put:
97
+ mock_put.return_value = mock.Mock(
98
+ status_code=200, raise_for_status=lambda: None
99
+ )
100
+ result = client.v1.files.upload_file(tmp_path)
101
+ assert result == "api-assets/id/video.mp4"
102
+ mock_put.assert_called_once_with(url=mock.ANY, content=data)
103
+
104
+ os.remove(tmp_path)
105
+
106
+
107
+ # Test error cases
108
+ def test_upload_file_nonexistent_file():
109
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
110
+
111
+ with pytest.raises(
112
+ FileNotFoundError, match="File not found: /nonexistent/file.mp4"
113
+ ):
114
+ client.v1.files.upload_file("/nonexistent/file.mp4")
115
+
116
+
117
+ def test_upload_file_unsupported_file_type():
118
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as tmp:
119
+ tmp.write(b"test data")
120
+ tmp_path = tmp.name
121
+
122
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
123
+
124
+ with pytest.raises(ValueError, match="Could not determine file type"):
125
+ client.v1.files.upload_file(tmp_path)
126
+
127
+ os.remove(tmp_path)
128
+
129
+
130
+ def test_upload_file_binary_io_without_name():
131
+ data = b"test data"
132
+ file_obj = io.BytesIO(data)
133
+ # Intentionally not setting name attribute
134
+
135
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
136
+
137
+ with pytest.raises(
138
+ ValueError, match="File-like object must have a 'name' attribute"
139
+ ):
140
+ client.v1.files.upload_file(file_obj)
141
+
142
+
143
+ def test_upload_file_binary_io_with_non_string_name():
144
+ data = b"test data"
145
+ file_obj = io.BytesIO(data)
146
+ file_obj.name = 123 # Non-string name
147
+
148
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
149
+
150
+ with pytest.raises(
151
+ ValueError, match="File-like object must have a 'name' attribute of type str"
152
+ ):
153
+ client.v1.files.upload_file(file_obj)
154
+
155
+
156
+ def test_upload_file_no_upload_url_returned():
157
+ data = b"test data"
158
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
159
+ tmp.write(data)
160
+ tmp_path = tmp.name
161
+
162
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
163
+
164
+ # Mock the upload_urls.create to return empty items
165
+ with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
166
+ mock_create.return_value = mock.Mock(items=[])
167
+
168
+ with pytest.raises(
169
+ ValueError, match="No upload URL was returned from the server"
170
+ ):
171
+ client.v1.files.upload_file(tmp_path)
172
+
173
+ os.remove(tmp_path)
174
+
175
+
176
+ def test_upload_file_http_error():
177
+ data = b"test data"
178
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
179
+ tmp.write(data)
180
+ tmp_path = tmp.name
181
+
182
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
183
+
184
+ with mock.patch("httpx.Client.put") as mock_put:
185
+ # Mock HTTP error response
186
+ mock_response = mock.Mock()
187
+ mock_response.raise_for_status.side_effect = Exception("Upload failed")
188
+ mock_put.return_value = mock_response
189
+
190
+ with pytest.raises(Exception, match="Upload failed"):
191
+ client.v1.files.upload_file(tmp_path)
192
+
193
+ os.remove(tmp_path)
194
+
195
+
196
+ # Test different file types to ensure proper type detection
197
+ def test_upload_different_file_types():
198
+ test_cases = [
199
+ (".mp4", "video"),
200
+ (".mov", "video"),
201
+ (".webm", "video"),
202
+ (".mp3", "audio"),
203
+ (".wav", "audio"),
204
+ (".png", "image"),
205
+ (".jpg", "image"),
206
+ (".jpeg", "image"),
207
+ ]
208
+
209
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
210
+
211
+ for extension, expected_type in test_cases:
212
+ data = b"test data"
213
+ with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as tmp:
214
+ tmp.write(data)
215
+ tmp_path = tmp.name
216
+
217
+ with mock.patch("httpx.Client.put") as mock_put:
218
+ mock_put.return_value = mock.Mock(
219
+ status_code=200, raise_for_status=lambda: None
220
+ )
221
+
222
+ # Mock the upload_urls.create to verify correct type is passed
223
+ with mock.patch.object(
224
+ client.v1.files.upload_urls, "create"
225
+ ) as mock_create:
226
+ mock_create.return_value = mock.Mock(
227
+ items=[
228
+ mock.Mock(
229
+ upload_url="https://test.com/upload",
230
+ file_path="api-assets/id/video.mp4",
231
+ )
232
+ ]
233
+ )
234
+
235
+ result = client.v1.files.upload_file(tmp_path)
236
+ assert result == "api-assets/id/video.mp4"
237
+
238
+ # Verify the correct type and extension were passed
239
+ call_args = mock_create.call_args
240
+ items = call_args.kwargs["items"]
241
+ assert len(items) == 1
242
+ assert items[0]["type_"] == expected_type
243
+ assert items[0]["extension"] == extension[1:] # without the dot
244
+
245
+ os.remove(tmp_path)
246
+
247
+
248
+ # Test URL handling - should skip upload and return URL as is
249
+ def test_upload_file_with_http_url():
250
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
251
+
252
+ http_url = "http://example.com/image.jpg"
253
+ result = client.v1.files.upload_file(http_url)
254
+
255
+ assert result == http_url
256
+
257
+
258
+ def test_upload_file_with_https_url():
259
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
260
+
261
+ https_url = "https://example.com/video.mp4"
262
+ result = client.v1.files.upload_file(https_url)
263
+
264
+ assert result == https_url
265
+
266
+
267
+ @pytest.mark.asyncio
268
+ async def test_async_upload_file_with_http_url():
269
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
270
+
271
+ http_url = "http://example.com/audio.mp3"
272
+ result = await client.v1.files.upload_file(http_url)
273
+
274
+ assert result == http_url
275
+
276
+
277
+ @pytest.mark.asyncio
278
+ async def test_async_upload_file_with_https_url():
279
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
280
+
281
+ https_url = "https://example.com/document.pdf"
282
+ result = await client.v1.files.upload_file(https_url)
283
+
284
+ assert result == https_url
285
+
286
+
287
+ # Test blob path handling - should skip upload and return blob path as is
288
+ def test_upload_file_with_blob_path():
289
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
290
+
291
+ blob_path = "api-assets/user123/image.jpg"
292
+ result = client.v1.files.upload_file(blob_path)
293
+
294
+ assert result == blob_path
295
+
296
+
297
+ def test_upload_file_with_blob_path_different_format():
298
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
299
+
300
+ blob_path = "api-assets/project456/video.mp4"
301
+ result = client.v1.files.upload_file(blob_path)
302
+
303
+ assert result == blob_path
304
+
305
+
306
+ @pytest.mark.asyncio
307
+ async def test_async_upload_file_with_blob_path():
308
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
309
+
310
+ blob_path = "api-assets/user789/audio.wav"
311
+ result = await client.v1.files.upload_file(blob_path)
312
+
313
+ assert result == blob_path
314
+
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_async_upload_file_with_blob_path_different_format():
318
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
319
+
320
+ blob_path = "api-assets/session101/photo.png"
321
+ result = await client.v1.files.upload_file(blob_path)
322
+
323
+ assert result == blob_path
324
+
325
+
326
+ # Test that URL and blob path handling doesn't make HTTP requests
327
+ def test_upload_file_with_url_does_not_make_http_requests():
328
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
329
+
330
+ with mock.patch("httpx.Client.put") as mock_put:
331
+ with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
332
+ result = client.v1.files.upload_file("https://example.com/file.jpg")
333
+
334
+ # Should not call upload_urls.create or make HTTP PUT request
335
+ mock_create.assert_not_called()
336
+ mock_put.assert_not_called()
337
+
338
+ assert result == "https://example.com/file.jpg"
339
+
340
+
341
+ def test_upload_file_with_blob_path_does_not_make_http_requests():
342
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
343
+
344
+ with mock.patch("httpx.Client.put") as mock_put:
345
+ with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
346
+ result = client.v1.files.upload_file("api-assets/user123/file.mp4")
347
+
348
+ # Should not call upload_urls.create or make HTTP PUT request
349
+ mock_create.assert_not_called()
350
+ mock_put.assert_not_called()
351
+
352
+ assert result == "api-assets/user123/file.mp4"
353
+
354
+
355
+ @pytest.mark.asyncio
356
+ async def test_async_upload_file_with_url_does_not_make_http_requests():
357
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
358
+
359
+ with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
360
+ with mock.patch.object(
361
+ client.v1.files.upload_urls, "create", new_callable=mock.AsyncMock
362
+ ) as mock_create:
363
+ result = await client.v1.files.upload_file("http://example.com/file.mp3")
364
+
365
+ # Should not call upload_urls.create or make HTTP PUT request
366
+ mock_create.assert_not_awaited()
367
+ mock_put.assert_not_awaited()
368
+
369
+ assert result == "http://example.com/file.mp3"
370
+
371
+
372
+ @pytest.mark.asyncio
373
+ async def test_async_upload_file_with_blob_path_does_not_make_http_requests():
374
+ client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
375
+
376
+ with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
377
+ with mock.patch.object(
378
+ client.v1.files.upload_urls, "create", new_callable=mock.AsyncMock
379
+ ) as mock_create:
380
+ result = await client.v1.files.upload_file("api-assets/user456/file.wav")
381
+
382
+ # Should not call upload_urls.create or make HTTP PUT request
383
+ mock_create.assert_not_awaited()
384
+ mock_put.assert_not_awaited()
385
+
386
+ assert result == "api-assets/user456/file.wav"
387
+
388
+
389
+ # Test file position preservation for file-like objects
390
+ def test_upload_file_preserves_file_position():
391
+ data = b"test data for position preservation"
392
+ file_obj = io.BytesIO(data)
393
+ file_obj.name = "test.mp4"
394
+
395
+ # Move to middle of file
396
+ file_obj.seek(5)
397
+ original_position = file_obj.tell()
398
+
399
+ client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
400
+
401
+ with mock.patch("httpx.Client.put") as mock_put:
402
+ mock_put.return_value = mock.Mock(
403
+ status_code=200, raise_for_status=lambda: None
404
+ )
405
+ client.v1.files.upload_file(file_obj)
406
+
407
+ # Verify position was restored
408
+ assert file_obj.tell() == original_position
409
+
410
+ # Verify the full content was uploaded (not just from position 5)
411
+ mock_put.assert_called_once_with(
412
+ url=mock.ANY,
413
+ content=data, # Full data, not data[5:]
414
+ )
@@ -1,3 +1,10 @@
1
+ # v1_files_upload_urls
2
+
3
+ ## Module Functions
4
+
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ <!-- CUSTOM DOCS END -->
1
8
 
2
9
  ### Generate asset upload urls <a name="create"></a>
3
10
 
@@ -69,3 +76,4 @@ res = await client.v1.files.upload_urls.create(
69
76
 
70
77
  ##### Example
71
78
  `{"items": [{"expires_at": "2024-07-25T16:56:21.932Z", "file_path": "api-assets/id/video.mp4", "upload_url": "https://videos.magichour.ai/api-assets/id/video.mp4?auth-value=1234567890"}, {"expires_at": "2024-07-25T16:56:21.932Z", "file_path": "api-assets/id/audio.mp3", "upload_url": "https://videos.magichour.ai/api-assets/id/audio.mp3?auth-value=1234567890"}]}`
79
+
@@ -1,3 +1,68 @@
1
+ # v1_image_background_remover
2
+
3
+ ## Module Functions
4
+
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ ### Image Background Remover 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.image_background_remover.generate(
35
+ assets={
36
+ "background_image_file_path": "/path/to/1234.png",
37
+ "image_file_path": "/path/to/1234.png",
38
+ },
39
+ name="Background Remover image",
40
+ wait_for_completion=True,
41
+ download_outputs=True,
42
+ download_directory="outputs"
43
+ )
44
+ ```
45
+
46
+ #### Asynchronous Client
47
+
48
+ ```python
49
+ from magic_hour import AsyncClient
50
+ from os import getenv
51
+
52
+ client = AsyncClient(token=getenv("API_TOKEN"))
53
+ res = await client.v1.image_background_remover.generate(
54
+ assets={
55
+ "background_image_file_path": "/path/to/1234.png",
56
+ "image_file_path": "/path/to/1234.png",
57
+ },
58
+ name="Background Remover image",
59
+ wait_for_completion=True,
60
+ download_outputs=True,
61
+ download_directory="outputs"
62
+ )
63
+ ```
64
+
65
+ <!-- CUSTOM DOCS END -->
1
66
 
2
67
  ### Image Background Remover <a name="create"></a>
3
68
 
@@ -10,6 +75,8 @@ Remove background from image. Each image costs 5 credits.
10
75
  | Parameter | Required | Description | Example |
11
76
  |-----------|:--------:|-------------|--------|
12
77
  | `assets` | ✓ | Provide the assets for background removal | `{"background_image_file_path": "api-assets/id/1234.png", "image_file_path": "api-assets/id/1234.png"}` |
78
+ | `└─ background_image_file_path` | ✗ | The image used as the new background for the image_file_path. This image will be resized to match the image in image_file_path. Please make sure the resolution between the images are similar. 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"` |
79
+ | `└─ image_file_path` | ✓ | The image to remove the background. 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
80
  | `name` | ✗ | The name of image. This value is mainly used for your own identification of the image. | `"Background Remover image"` |
14
81
 
15
82
  #### Synchronous Client
@@ -53,3 +120,4 @@ res = await client.v1.image_background_remover.create(
53
120
 
54
121
  ##### Example
55
122
  `{"credits_charged": 5, "frame_cost": 5, "id": "cuid-example"}`
123
+
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import typing
2
3
 
3
4
  from magic_hour.core import (
@@ -8,13 +9,82 @@ 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 ImageBackgroundRemoverClient:
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.V1ImageBackgroundRemoverGenerateBodyAssets,
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 background removed image (alias for create with additional functionality).
42
+
43
+ Remove background from image. Each removal 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 background removal
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 Image Background Remover API with the downloaded paths if `download_outputs` is True.
55
+
56
+ Examples:
57
+ ```py
58
+ response = client.v1.image_background_remover.generate(
59
+ assets={"image_file_path": "path/to/image.png"},
60
+ name="Background Removed Image",
61
+ wait_for_completion=True,
62
+ download_outputs=True,
63
+ download_directory="outputs/",
64
+ )
65
+ ```
66
+ """
67
+
68
+ file_client = FilesClient(base_client=self._base_client)
69
+
70
+ image_file_path = assets["image_file_path"]
71
+ assets["image_file_path"] = file_client.upload_file(file=image_file_path)
72
+
73
+ create_response = self.create(
74
+ assets=assets, name=name, request_options=request_options
75
+ )
76
+ logger.info(f"Image Background Remover response: {create_response}")
77
+
78
+ image_projects_client = ImageProjectsClient(base_client=self._base_client)
79
+ response = image_projects_client.check_result(
80
+ id=create_response.id,
81
+ wait_for_completion=wait_for_completion,
82
+ download_outputs=download_outputs,
83
+ download_directory=download_directory,
84
+ )
85
+
86
+ return response
87
+
18
88
  def create(
19
89
  self,
20
90
  *,
@@ -72,6 +142,66 @@ class AsyncImageBackgroundRemoverClient:
72
142
  def __init__(self, *, base_client: AsyncBaseClient):
73
143
  self._base_client = base_client
74
144
 
145
+ async def generate(
146
+ self,
147
+ *,
148
+ assets: params.V1ImageBackgroundRemoverGenerateBodyAssets,
149
+ name: typing.Union[
150
+ typing.Optional[str], type_utils.NotGiven
151
+ ] = type_utils.NOT_GIVEN,
152
+ wait_for_completion: bool = True,
153
+ download_outputs: bool = True,
154
+ download_directory: typing.Optional[str] = None,
155
+ request_options: typing.Optional[RequestOptions] = None,
156
+ ):
157
+ """
158
+ Generate background removed image (alias for create with additional functionality).
159
+
160
+ Remove background from image. Each removal costs 5 credits.
161
+
162
+ Args:
163
+ name: The name of image. This value is mainly used for your own identification of the image.
164
+ assets: Provide the assets for background removal
165
+ wait_for_completion: Whether to wait for the image project to complete
166
+ download_outputs: Whether to download the outputs
167
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
168
+ request_options: Additional options to customize the HTTP request
169
+
170
+ Returns:
171
+ V1ImageProjectsGetResponseWithDownloads: The response from the Image Background Remover API with the downloaded paths if `download_outputs` is True.
172
+
173
+ Examples:
174
+ ```py
175
+ response = await client.v1.image_background_remover.generate(
176
+ assets={"image_file_path": "path/to/image.png"},
177
+ name="Background Removed Image",
178
+ wait_for_completion=True,
179
+ download_outputs=True,
180
+ download_directory="outputs/",
181
+ )
182
+ ```
183
+ """
184
+
185
+ file_client = AsyncFilesClient(base_client=self._base_client)
186
+
187
+ image_file_path = assets["image_file_path"]
188
+ assets["image_file_path"] = await file_client.upload_file(file=image_file_path)
189
+
190
+ create_response = await self.create(
191
+ assets=assets, name=name, request_options=request_options
192
+ )
193
+ logger.info(f"Image Background Remover response: {create_response}")
194
+
195
+ image_projects_client = AsyncImageProjectsClient(base_client=self._base_client)
196
+ response = await image_projects_client.check_result(
197
+ id=create_response.id,
198
+ wait_for_completion=wait_for_completion,
199
+ download_outputs=download_outputs,
200
+ download_directory=download_directory,
201
+ )
202
+
203
+ return response
204
+
75
205
  async def create(
76
206
  self,
77
207
  *,