magic_hour 0.41.0__py3-none-any.whl → 0.42.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of magic_hour might be problematic. Click here for more details.

@@ -23,6 +23,7 @@ def download_files_sync(
23
23
  downloads: Union[
24
24
  List[models.V1ImageProjectsGetResponseDownloadsItem],
25
25
  List[models.V1VideoProjectsGetResponseDownloadsItem],
26
+ List[models.V1AudioProjectsGetResponseDownloadsItem],
26
27
  ],
27
28
  download_directory: Union[str, None] = None,
28
29
  ) -> List[str]:
@@ -51,6 +52,7 @@ async def download_files_async(
51
52
  downloads: Union[
52
53
  List[models.V1ImageProjectsGetResponseDownloadsItem],
53
54
  List[models.V1VideoProjectsGetResponseDownloadsItem],
55
+ List[models.V1AudioProjectsGetResponseDownloadsItem],
54
56
  ],
55
57
  download_directory: Union[str, None] = None,
56
58
  ) -> List[str]:
@@ -2,6 +2,62 @@
2
2
 
3
3
  ## Module Functions
4
4
 
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ ### Ai Talking Photo Generate Workflow <a name="generate"></a>
8
+
9
+ The workflow performs the following action
10
+
11
+ 1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself
12
+ 2. trigger a generation
13
+ 3. poll for a completion status. This is configurable
14
+ 4. if success, download the output to local directory
15
+
16
+ > [!TIP]
17
+ > This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions.
18
+
19
+ #### Parameters
20
+
21
+ In Additional to the parameters listed in the `.create` section below, `.generate` introduces 3 new parameters:
22
+
23
+ - `wait_for_completion` (bool, default True): Whether to wait for the project to complete.
24
+ - `download_outputs` (bool, default True): Whether to download the generated files
25
+ - `download_directory` (str, optional): Directory to save downloaded files (defaults to current directory)
26
+
27
+ #### Synchronous Client
28
+
29
+ ```python
30
+ from magic_hour import Client
31
+ from os import getenv
32
+
33
+ client = Client(token=getenv("API_TOKEN"))
34
+ res = client.v1.ai_talking_photo.generate(
35
+ style={"prompt": "Hello, how are you?", "voice_name": "Elon Musk"},
36
+ name="Voice Generator audio",
37
+ wait_for_completion=True,
38
+ download_outputs=True,
39
+ download_directory="outputs"
40
+ )
41
+ ```
42
+
43
+ #### Asynchronous Client
44
+
45
+ ```python
46
+ from magic_hour import AsyncClient
47
+ from os import getenv
48
+
49
+ client = AsyncClient(token=getenv("API_TOKEN"))
50
+ res = await client.v1.ai_talking_photo.generate(
51
+ style={"prompt": "Hello, how are you?", "voice_name": "Elon Musk"},
52
+ name="Voice Generator audio",
53
+ wait_for_completion=True,
54
+ download_outputs=True,
55
+ download_directory="outputs"
56
+ )
57
+ ```
58
+
59
+ <!-- CUSTOM DOCS END -->
60
+
5
61
  ### AI Voice Generator <a name="create"></a>
6
62
 
7
63
  Generate speech from text. Each character costs 0.05 credits. The cost is rounded up to the nearest whole number.
@@ -1,5 +1,10 @@
1
1
  import typing
2
2
 
3
+ from magic_hour.helpers.logger import get_sdk_logger
4
+ from magic_hour.resources.v1.audio_projects.client import (
5
+ AsyncAudioProjectsClient,
6
+ AudioProjectsClient,
7
+ )
3
8
  from magic_hour.types import models, params
4
9
  from make_api_request import (
5
10
  AsyncBaseClient,
@@ -11,10 +16,70 @@ from make_api_request import (
11
16
  )
12
17
 
13
18
 
19
+ logger = get_sdk_logger(__name__)
20
+
21
+
14
22
  class AiVoiceGeneratorClient:
15
23
  def __init__(self, *, base_client: SyncBaseClient):
16
24
  self._base_client = base_client
17
25
 
26
+ def generate(
27
+ self,
28
+ *,
29
+ style: params.V1AiVoiceGeneratorCreateBodyStyle,
30
+ name: typing.Union[
31
+ typing.Optional[str], type_utils.NotGiven
32
+ ] = type_utils.NOT_GIVEN,
33
+ wait_for_completion: bool = True,
34
+ download_outputs: bool = True,
35
+ download_directory: typing.Optional[str] = None,
36
+ request_options: typing.Optional[RequestOptions] = None,
37
+ ):
38
+ """
39
+ Generate AI voice (alias for create with additional functionality).
40
+
41
+ Generate speech from text. Each character costs 0.05 credits. The cost is rounded up to the nearest whole number.
42
+
43
+ Args:
44
+ style: The content used to generate speech.
45
+ name: The name of audio. This value is mainly used for your own identification of the audio.
46
+ wait_for_completion: Whether to wait for the audio project to complete
47
+ download_outputs: Whether to download the outputs
48
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
49
+ request_options: Additional options to customize the HTTP request
50
+
51
+ Returns:
52
+ V1AudioProjectsGetResponseWithDownloads: The response from the AI Voice Generator API with the downloaded paths if `download_outputs` is True.
53
+
54
+ Examples:
55
+ ```py
56
+ response = client.v1.ai_voice_generator.generate(
57
+ style={"prompt": "Hello, how are you?", "voice_name": "Elon Musk"},
58
+ name="Generated Voice",
59
+ wait_for_completion=True,
60
+ download_outputs=True,
61
+ download_directory="outputs/",
62
+ )
63
+ ```
64
+ """
65
+
66
+ create_response = self.create(
67
+ style=style,
68
+ name=name,
69
+ request_options=request_options,
70
+ )
71
+ logger.info(f"AI Voice Generator response: {create_response}")
72
+
73
+ audio_projects_client = AudioProjectsClient(base_client=self._base_client)
74
+ response = audio_projects_client.check_result(
75
+ id=create_response.id,
76
+ wait_for_completion=wait_for_completion,
77
+ download_outputs=download_outputs,
78
+ download_directory=download_directory,
79
+ )
80
+
81
+ return response
82
+
18
83
  def create(
19
84
  self,
20
85
  *,
@@ -69,6 +134,63 @@ class AsyncAiVoiceGeneratorClient:
69
134
  def __init__(self, *, base_client: AsyncBaseClient):
70
135
  self._base_client = base_client
71
136
 
137
+ async def generate(
138
+ self,
139
+ *,
140
+ style: params.V1AiVoiceGeneratorCreateBodyStyle,
141
+ name: typing.Union[
142
+ typing.Optional[str], type_utils.NotGiven
143
+ ] = type_utils.NOT_GIVEN,
144
+ wait_for_completion: bool = True,
145
+ download_outputs: bool = True,
146
+ download_directory: typing.Optional[str] = None,
147
+ request_options: typing.Optional[RequestOptions] = None,
148
+ ):
149
+ """
150
+ Generate AI voice (alias for create with additional functionality).
151
+
152
+ Generate speech from text. Each character costs 0.05 credits. The cost is rounded up to the nearest whole number.
153
+
154
+ Args:
155
+ style: The content used to generate speech.
156
+ name: The name of audio. This value is mainly used for your own identification of the audio.
157
+ wait_for_completion: Whether to wait for the audio project to complete
158
+ download_outputs: Whether to download the outputs
159
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
160
+ request_options: Additional options to customize the HTTP request
161
+
162
+ Returns:
163
+ V1AudioProjectsGetResponseWithDownloads: The response from the AI Voice Generator API with the downloaded paths if `download_outputs` is True.
164
+
165
+ Examples:
166
+ ```py
167
+ response = await client.v1.ai_voice_generator.generate(
168
+ style={"prompt": "Hello, how are you?", "voice_name": "Elon Musk"},
169
+ name="Generated Voice",
170
+ wait_for_completion=True,
171
+ download_outputs=True,
172
+ download_directory="outputs/",
173
+ )
174
+ ```
175
+ """
176
+
177
+ create_response = await self.create(
178
+ style=style,
179
+ name=name,
180
+ request_options=request_options,
181
+ )
182
+ logger.info(f"AI Voice Generator response: {create_response}")
183
+
184
+ audio_projects_client = AsyncAudioProjectsClient(base_client=self._base_client)
185
+ response = await audio_projects_client.check_result(
186
+ id=create_response.id,
187
+ wait_for_completion=wait_for_completion,
188
+ download_outputs=download_outputs,
189
+ download_directory=download_directory,
190
+ )
191
+
192
+ return response
193
+
72
194
  async def create(
73
195
  self,
74
196
  *,
@@ -2,6 +2,53 @@
2
2
 
3
3
  ## Module Functions
4
4
 
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ ### Check results <a name="check-result"></a>
8
+
9
+ Poll the details API to check on the status of the rendering. Optionally can also download the output
10
+
11
+ #### Parameters
12
+
13
+ | Parameter | Required | Description | Example |
14
+ | --------------------- | :------: | ---------------------------------------------------------------------------------------------------- | ---------------- |
15
+ | `id` | ✓ | Unique ID of the audio project. This value is returned by all of the POST APIs that create an audio. | `"cuid-example"` |
16
+ | `wait_for_completion` | ✗ | Whether to wait for the project to complete. | `True` |
17
+ | `download_outputs` | ✗ | Whether to download the generated files | `True` |
18
+ | `download_directory` | ✗ | Directory to save downloaded files (defaults to current directory) | `"./outputs"` |
19
+
20
+ #### Synchronous Client
21
+
22
+ ```python
23
+ from magic_hour import Client
24
+ from os import getenv
25
+
26
+ client = Client(token=getenv("API_TOKEN"))
27
+ res = client.v1.audio_projects.check_result(
28
+ id="cuid-example",
29
+ wait_for_completion=True,
30
+ download_outputs=True,
31
+ download_directory="outputs",
32
+ )
33
+
34
+ ```
35
+
36
+ #### Asynchronous Client
37
+
38
+ ```python
39
+ from magic_hour import AsyncClient
40
+ from os import getenv
41
+
42
+ client = AsyncClient(token=getenv("API_TOKEN"))
43
+ res = await client.v1.audio_projects.check_result(
44
+ id="cuid-example",
45
+ wait_for_completion=True,
46
+ download_outputs=True,
47
+ download_directory="outputs",
48
+ )
49
+ ```
50
+
51
+ <!-- CUSTOM DOCS END -->
5
52
  ### Delete audio <a name="delete"></a>
6
53
 
7
54
  Permanently delete the rendered audio file(s). This action is not reversible, please be sure before deleting.
@@ -1,4 +1,12 @@
1
- from .client import AsyncAudioProjectsClient, AudioProjectsClient
1
+ from .client import (
2
+ AsyncAudioProjectsClient,
3
+ AudioProjectsClient,
4
+ V1AudioProjectsGetResponseWithDownloads,
5
+ )
2
6
 
3
7
 
4
- __all__ = ["AsyncAudioProjectsClient", "AudioProjectsClient"]
8
+ __all__ = [
9
+ "AsyncAudioProjectsClient",
10
+ "AudioProjectsClient",
11
+ "V1AudioProjectsGetResponseWithDownloads",
12
+ ]
@@ -1,5 +1,10 @@
1
+ import os
2
+ import pydantic
3
+ import time
1
4
  import typing
2
5
 
6
+ from magic_hour.helpers.download import download_files_async, download_files_sync
7
+ from magic_hour.helpers.logger import get_sdk_logger
3
8
  from magic_hour.types import models
4
9
  from make_api_request import (
5
10
  AsyncBaseClient,
@@ -9,10 +14,83 @@ from make_api_request import (
9
14
  )
10
15
 
11
16
 
17
+ logger = get_sdk_logger(__name__)
18
+
19
+
20
+ class V1AudioProjectsGetResponseWithDownloads(models.V1AudioProjectsGetResponse):
21
+ downloaded_paths: typing.Optional[typing.List[str]] = pydantic.Field(
22
+ default=None, alias="downloaded_paths"
23
+ )
24
+ """
25
+ The paths to the downloaded files.
26
+
27
+ This field is only populated if `download_outputs` is True and the audio project is complete.
28
+ """
29
+
30
+
12
31
  class AudioProjectsClient:
13
32
  def __init__(self, *, base_client: SyncBaseClient):
14
33
  self._base_client = base_client
15
34
 
35
+ def check_result(
36
+ self,
37
+ id: str,
38
+ wait_for_completion: bool,
39
+ download_outputs: bool,
40
+ download_directory: typing.Optional[str] = None,
41
+ ) -> V1AudioProjectsGetResponseWithDownloads:
42
+ """
43
+ Check the result of an audio project with optional waiting and downloading.
44
+
45
+ This method retrieves the status of an audio project and optionally waits for completion
46
+ and downloads the output files.
47
+
48
+ Args:
49
+ id: Unique ID of the audio project
50
+ wait_for_completion: Whether to wait for the audio project to complete
51
+ download_outputs: Whether to download the outputs
52
+ download_directory: The directory to download the outputs to. If not provided,
53
+ the outputs will be downloaded to the current working directory
54
+
55
+ Returns:
56
+ V1AudioProjectsGetResponseWithDownloads: The audio project response with optional
57
+ downloaded file paths included
58
+ """
59
+ api_response = self.get(id=id)
60
+ if not wait_for_completion:
61
+ response = V1AudioProjectsGetResponseWithDownloads(
62
+ **api_response.model_dump()
63
+ )
64
+ return response
65
+
66
+ poll_interval = float(os.getenv("MAGIC_HOUR_POLL_INTERVAL", "0.5"))
67
+
68
+ status = api_response.status
69
+
70
+ while status not in ["complete", "error", "canceled"]:
71
+ api_response = self.get(id=id)
72
+ status = api_response.status
73
+ time.sleep(poll_interval)
74
+
75
+ if api_response.status != "complete":
76
+ log = logger.error if api_response.status == "error" else logger.info
77
+ log(
78
+ f"Audio project {id} has status {api_response.status}: {api_response.error}"
79
+ )
80
+ return V1AudioProjectsGetResponseWithDownloads(**api_response.model_dump())
81
+
82
+ if not download_outputs:
83
+ return V1AudioProjectsGetResponseWithDownloads(**api_response.model_dump())
84
+
85
+ downloaded_paths = download_files_sync(
86
+ downloads=api_response.downloads,
87
+ download_directory=download_directory,
88
+ )
89
+
90
+ return V1AudioProjectsGetResponseWithDownloads(
91
+ **api_response.model_dump(), downloaded_paths=downloaded_paths
92
+ )
93
+
16
94
  def delete(
17
95
  self, *, id: str, request_options: typing.Optional[RequestOptions] = None
18
96
  ) -> None:
@@ -95,6 +173,65 @@ class AsyncAudioProjectsClient:
95
173
  def __init__(self, *, base_client: AsyncBaseClient):
96
174
  self._base_client = base_client
97
175
 
176
+ async def check_result(
177
+ self,
178
+ id: str,
179
+ wait_for_completion: bool,
180
+ download_outputs: bool,
181
+ download_directory: typing.Optional[str] = None,
182
+ ) -> V1AudioProjectsGetResponseWithDownloads:
183
+ """
184
+ Check the result of an audio project with optional waiting and downloading.
185
+
186
+ This method retrieves the status of an audio project and optionally waits for completion
187
+ and downloads the output files.
188
+
189
+ Args:
190
+ id: Unique ID of the audio project
191
+ wait_for_completion: Whether to wait for the audio project to complete
192
+ download_outputs: Whether to download the outputs
193
+ download_directory: The directory to download the outputs to. If not provided,
194
+ the outputs will be downloaded to the current working directory
195
+
196
+ Returns:
197
+ V1AudioProjectsGetResponseWithDownloads: The audio project response with optional
198
+ downloaded file paths included
199
+ """
200
+ api_response = await self.get(id=id)
201
+ if not wait_for_completion:
202
+ response = V1AudioProjectsGetResponseWithDownloads(
203
+ **api_response.model_dump()
204
+ )
205
+ return response
206
+
207
+ poll_interval = float(os.getenv("MAGIC_HOUR_POLL_INTERVAL", "0.5"))
208
+
209
+ status = api_response.status
210
+
211
+ while status not in ["complete", "error", "canceled"]:
212
+ api_response = await self.get(id=id)
213
+ status = api_response.status
214
+ time.sleep(poll_interval)
215
+
216
+ if api_response.status != "complete":
217
+ log = logger.error if api_response.status == "error" else logger.info
218
+ log(
219
+ f"Audio project {id} has status {api_response.status}: {api_response.error}"
220
+ )
221
+ return V1AudioProjectsGetResponseWithDownloads(**api_response.model_dump())
222
+
223
+ if not download_outputs:
224
+ return V1AudioProjectsGetResponseWithDownloads(**api_response.model_dump())
225
+
226
+ downloaded_paths = await download_files_async(
227
+ downloads=api_response.downloads,
228
+ download_directory=download_directory,
229
+ )
230
+
231
+ return V1AudioProjectsGetResponseWithDownloads(
232
+ **api_response.model_dump(), downloaded_paths=downloaded_paths
233
+ )
234
+
98
235
  async def delete(
99
236
  self, *, id: str, request_options: typing.Optional[RequestOptions] = None
100
237
  ) -> None:
@@ -0,0 +1,520 @@
1
+ import datetime
2
+ import pytest
3
+ import httpx
4
+ from pathlib import Path
5
+ from typing import Any, Generator, Literal, Union, List
6
+ from unittest.mock import Mock, AsyncMock
7
+
8
+ from magic_hour.types import models
9
+ from magic_hour.resources.v1.audio_projects.client import (
10
+ AudioProjectsClient,
11
+ AsyncAudioProjectsClient,
12
+ )
13
+
14
+
15
+ class DummyResponse(models.V1AudioProjectsGetResponse):
16
+ """Helper response with defaults"""
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ status: Literal[
22
+ "complete", "queued", "rendering", "error", "canceled"
23
+ ] = "complete",
24
+ download_url: Union[str, None] = None,
25
+ error: Union[str, None] = None,
26
+ ):
27
+ # Create error object if error string is provided
28
+ error_obj = None
29
+ if error:
30
+ error_obj = models.V1AudioProjectsGetResponseError(
31
+ code="TEST_ERROR", message=error
32
+ )
33
+
34
+ super().__init__(
35
+ id="test-id",
36
+ created_at=datetime.datetime.now().isoformat(),
37
+ credits_charged=0,
38
+ downloads=[
39
+ models.V1AudioProjectsGetResponseDownloadsItem(
40
+ url=download_url, expires_at="2024-01-01T00:00:00Z"
41
+ )
42
+ ]
43
+ if download_url
44
+ else [],
45
+ enabled=True,
46
+ error=error_obj,
47
+ name="test-name",
48
+ status=status,
49
+ type="test-type",
50
+ )
51
+
52
+
53
+ @pytest.fixture
54
+ def mock_base_client() -> Generator[Mock, None, None]:
55
+ yield Mock()
56
+
57
+
58
+ @pytest.fixture
59
+ def mock_async_base_client() -> Generator[AsyncMock, None, None]:
60
+ yield AsyncMock()
61
+
62
+
63
+ def test_delete_calls_base_client(mock_base_client: Mock) -> None:
64
+ client = AudioProjectsClient(base_client=mock_base_client)
65
+ client.delete(id="123")
66
+
67
+ mock_base_client.request.assert_called_once()
68
+ call = mock_base_client.request.call_args[1]
69
+ assert call["method"] == "DELETE"
70
+ assert "/v1/audio-projects/123" in call["path"]
71
+
72
+
73
+ def test_get_calls_base_client(mock_base_client: Mock) -> None:
74
+ client = AudioProjectsClient(base_client=mock_base_client)
75
+ mock_base_client.request.return_value = DummyResponse()
76
+
77
+ resp = client.get(id="abc")
78
+
79
+ mock_base_client.request.assert_called_once()
80
+ assert isinstance(resp, models.V1AudioProjectsGetResponse)
81
+ assert resp.id == "test-id"
82
+
83
+
84
+ def test_check_result_no_wait_no_download(mock_base_client: Mock) -> None:
85
+ client = AudioProjectsClient(base_client=mock_base_client)
86
+ mock_base_client.request.return_value = DummyResponse(status="queued")
87
+
88
+ resp = client.check_result(
89
+ id="xyz",
90
+ wait_for_completion=False,
91
+ download_outputs=False,
92
+ )
93
+
94
+ assert resp.downloaded_paths is None
95
+
96
+
97
+ def test_check_result_wait_until_complete(
98
+ monkeypatch: Any, mock_base_client: Mock
99
+ ) -> None:
100
+ client = AudioProjectsClient(base_client=mock_base_client)
101
+
102
+ # First calls return queued, then complete
103
+ mock_base_client.request.side_effect = [
104
+ DummyResponse(status="queued"),
105
+ DummyResponse(status="queued"),
106
+ DummyResponse(status="complete"),
107
+ ]
108
+
109
+ monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
110
+
111
+ resp = client.check_result(
112
+ id="xyz", wait_for_completion=True, download_outputs=False
113
+ )
114
+
115
+ assert resp.status == "complete"
116
+ assert resp.downloaded_paths is None
117
+
118
+
119
+ def test_check_result_download_outputs(
120
+ tmp_path: Path, mock_base_client: Mock, monkeypatch: Any
121
+ ) -> None:
122
+ client = AudioProjectsClient(base_client=mock_base_client)
123
+
124
+ file_url = "https://example.com/file.mp3"
125
+ mock_base_client.request.return_value = DummyResponse(
126
+ status="complete",
127
+ download_url=file_url,
128
+ )
129
+
130
+ # Create a mock response for httpx
131
+ mock_request = httpx.Request("GET", "https://example.com/file.mp3")
132
+ mock_response = httpx.Response(200, content=b"fake mp3", request=mock_request)
133
+
134
+ # Mock the httpx.Client class
135
+ class MockClient:
136
+ def __init__(self):
137
+ pass
138
+
139
+ def __enter__(self) -> "MockClient":
140
+ return self
141
+
142
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
143
+ pass
144
+
145
+ def get(self, url: str) -> httpx.Response:
146
+ return mock_response
147
+
148
+ monkeypatch.setattr(httpx, "Client", MockClient)
149
+
150
+ resp = client.check_result(
151
+ id="xyz",
152
+ wait_for_completion=True,
153
+ download_outputs=True,
154
+ download_directory=str(tmp_path),
155
+ )
156
+
157
+ assert resp.status == "complete"
158
+ assert resp.downloaded_paths
159
+ saved_file = Path(resp.downloaded_paths[0])
160
+ assert saved_file.exists()
161
+ assert saved_file.read_bytes() == b"fake mp3"
162
+
163
+
164
+ def test_check_result_error_status(mock_base_client: Mock) -> None:
165
+ client = AudioProjectsClient(base_client=mock_base_client)
166
+ mock_base_client.request.return_value = DummyResponse(status="error", error="Boom!")
167
+
168
+ resp = client.check_result(
169
+ id="err", wait_for_completion=True, download_outputs=False
170
+ )
171
+ assert resp.status == "error"
172
+ assert resp.error is not None
173
+ assert resp.error.message == "Boom!"
174
+ assert resp.downloaded_paths is None
175
+
176
+
177
+ def test_check_result_canceled_status(mock_base_client: Mock) -> None:
178
+ client = AudioProjectsClient(base_client=mock_base_client)
179
+ mock_base_client.request.return_value = DummyResponse(status="canceled")
180
+
181
+ resp = client.check_result(
182
+ id="cancel", wait_for_completion=True, download_outputs=False
183
+ )
184
+ assert resp.status == "canceled"
185
+ assert resp.downloaded_paths is None
186
+
187
+
188
+ def test_check_result_poll_interval_default(
189
+ mock_base_client: Mock, monkeypatch: Any
190
+ ) -> None:
191
+ client = AudioProjectsClient(base_client=mock_base_client)
192
+
193
+ # First calls return queued, then complete
194
+ mock_base_client.request.side_effect = [
195
+ DummyResponse(status="queued"),
196
+ DummyResponse(status="complete"),
197
+ ]
198
+
199
+ # Mock time.sleep to track calls
200
+ sleep_calls: List[float] = []
201
+
202
+ def mock_sleep(seconds: float) -> None:
203
+ sleep_calls.append(seconds)
204
+
205
+ monkeypatch.setattr("time.sleep", mock_sleep)
206
+
207
+ resp = client.check_result(
208
+ id="xyz", wait_for_completion=True, download_outputs=False
209
+ )
210
+
211
+ assert resp.status == "complete"
212
+ # Should have slept once with default interval (0.5)
213
+ assert len(sleep_calls) == 1
214
+ assert sleep_calls[0] == 0.5
215
+
216
+
217
+ def test_check_result_poll_interval_custom(
218
+ mock_base_client: Mock, monkeypatch: Any
219
+ ) -> None:
220
+ client = AudioProjectsClient(base_client=mock_base_client)
221
+
222
+ # Set custom poll interval
223
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "1.0")
224
+
225
+ # First calls return queued, then complete
226
+ mock_base_client.request.side_effect = [
227
+ DummyResponse(status="queued"),
228
+ DummyResponse(status="complete"),
229
+ ]
230
+
231
+ # Mock time.sleep to track calls
232
+ sleep_calls: List[float] = []
233
+
234
+ def mock_sleep(seconds: float) -> None:
235
+ sleep_calls.append(seconds)
236
+
237
+ monkeypatch.setattr("time.sleep", mock_sleep)
238
+
239
+ resp = client.check_result(
240
+ id="xyz", wait_for_completion=True, download_outputs=False
241
+ )
242
+
243
+ assert resp.status == "complete"
244
+ # Should have slept once with custom interval (1.0)
245
+ assert len(sleep_calls) == 1
246
+ assert sleep_calls[0] == 1.0
247
+
248
+
249
+ def test_check_result_poll_interval_multiple_polls(
250
+ mock_base_client: Mock, monkeypatch: Any
251
+ ) -> None:
252
+ client = AudioProjectsClient(base_client=mock_base_client)
253
+
254
+ # Set custom poll interval
255
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "0.1")
256
+
257
+ # Multiple calls return queued before complete
258
+ mock_base_client.request.side_effect = [
259
+ DummyResponse(status="queued"),
260
+ DummyResponse(status="queued"),
261
+ DummyResponse(status="queued"),
262
+ DummyResponse(status="complete"),
263
+ ]
264
+
265
+ # Mock time.sleep to track calls
266
+ sleep_calls: List[float] = []
267
+
268
+ def mock_sleep(seconds: float) -> None:
269
+ sleep_calls.append(seconds)
270
+
271
+ monkeypatch.setattr("time.sleep", mock_sleep)
272
+
273
+ resp = client.check_result(
274
+ id="xyz", wait_for_completion=True, download_outputs=False
275
+ )
276
+
277
+ assert resp.status == "complete"
278
+ # Should have slept 3 times with custom interval (0.1)
279
+ assert len(sleep_calls) == 3
280
+ assert all(sleep_time == 0.1 for sleep_time in sleep_calls)
281
+
282
+
283
+ @pytest.mark.asyncio
284
+ async def test_async_delete_calls_base_client(
285
+ mock_async_base_client: AsyncMock,
286
+ ) -> None:
287
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
288
+ await client.delete(id="456")
289
+
290
+ mock_async_base_client.request.assert_called_once()
291
+ call = mock_async_base_client.request.call_args[1]
292
+ assert call["method"] == "DELETE"
293
+ assert "/v1/audio-projects/456" in call["path"]
294
+
295
+
296
+ @pytest.mark.asyncio
297
+ async def test_async_get_calls_base_client(mock_async_base_client: AsyncMock) -> None:
298
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
299
+ mock_async_base_client.request.return_value = DummyResponse()
300
+
301
+ resp = await client.get(id="zzz")
302
+
303
+ mock_async_base_client.request.assert_called_once()
304
+ assert isinstance(resp, models.V1AudioProjectsGetResponse)
305
+ assert resp.id == "test-id"
306
+
307
+
308
+ @pytest.mark.asyncio
309
+ async def test_async_check_result_no_wait_no_download(
310
+ mock_async_base_client: AsyncMock,
311
+ ) -> None:
312
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
313
+ mock_async_base_client.request.return_value = DummyResponse(status="queued")
314
+
315
+ resp = await client.check_result(
316
+ id="xyz",
317
+ wait_for_completion=False,
318
+ download_outputs=False,
319
+ )
320
+
321
+ assert resp.downloaded_paths is None
322
+
323
+
324
+ @pytest.mark.asyncio
325
+ async def test_async_check_result_wait_until_complete(
326
+ mock_async_base_client: AsyncMock, monkeypatch: Any
327
+ ) -> None:
328
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
329
+
330
+ # First calls return queued, then complete
331
+ mock_async_base_client.request.side_effect = [
332
+ DummyResponse(status="queued"),
333
+ DummyResponse(status="queued"),
334
+ DummyResponse(status="complete"),
335
+ ]
336
+
337
+ monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
338
+
339
+ resp = await client.check_result(
340
+ id="xyz", wait_for_completion=True, download_outputs=False
341
+ )
342
+
343
+ assert resp.status == "complete"
344
+ assert resp.downloaded_paths is None
345
+
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_async_check_result_download_outputs(
349
+ tmp_path: Path, mock_async_base_client: AsyncMock, monkeypatch: Any
350
+ ) -> None:
351
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
352
+
353
+ file_url = "https://example.com/file.mp3"
354
+ mock_async_base_client.request.return_value = DummyResponse(
355
+ status="complete",
356
+ download_url=file_url,
357
+ )
358
+
359
+ # Create a mock response for httpx
360
+ mock_request = httpx.Request("GET", "https://example.com/file.mp3")
361
+ mock_response = httpx.Response(200, content=b"fake mp3", request=mock_request)
362
+
363
+ # Mock the httpx.AsyncClient class
364
+ class MockAsyncClient:
365
+ def __init__(self):
366
+ pass
367
+
368
+ async def __aenter__(self) -> "MockAsyncClient":
369
+ return self
370
+
371
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
372
+ pass
373
+
374
+ async def get(self, url: str) -> httpx.Response:
375
+ return mock_response
376
+
377
+ monkeypatch.setattr(httpx, "AsyncClient", MockAsyncClient)
378
+
379
+ resp = await client.check_result(
380
+ id="xyz",
381
+ wait_for_completion=True,
382
+ download_outputs=True,
383
+ download_directory=str(tmp_path),
384
+ )
385
+
386
+ assert resp.status == "complete"
387
+ assert resp.downloaded_paths
388
+ saved_file = Path(resp.downloaded_paths[0])
389
+ assert saved_file.exists()
390
+ assert saved_file.read_bytes() == b"fake mp3"
391
+
392
+
393
+ @pytest.mark.asyncio
394
+ async def test_async_check_result_error_status(
395
+ mock_async_base_client: AsyncMock,
396
+ ) -> None:
397
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
398
+ mock_async_base_client.request.return_value = DummyResponse(
399
+ status="error", error="Boom!"
400
+ )
401
+
402
+ resp = await client.check_result(
403
+ id="err", wait_for_completion=True, download_outputs=False
404
+ )
405
+ assert resp.status == "error"
406
+ assert resp.error is not None
407
+ assert resp.error.message == "Boom!"
408
+ assert resp.downloaded_paths is None
409
+
410
+
411
+ @pytest.mark.asyncio
412
+ async def test_async_check_result_canceled_status(
413
+ mock_async_base_client: AsyncMock,
414
+ ) -> None:
415
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
416
+ mock_async_base_client.request.return_value = DummyResponse(status="canceled")
417
+
418
+ resp = await client.check_result(
419
+ id="cancel", wait_for_completion=True, download_outputs=False
420
+ )
421
+ assert resp.status == "canceled"
422
+ assert resp.downloaded_paths is None
423
+
424
+
425
+ @pytest.mark.asyncio
426
+ async def test_async_check_result_poll_interval_default(
427
+ mock_async_base_client: AsyncMock, monkeypatch: Any
428
+ ) -> None:
429
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
430
+
431
+ # First calls return queued, then complete
432
+ mock_async_base_client.request.side_effect = [
433
+ DummyResponse(status="queued"),
434
+ DummyResponse(status="complete"),
435
+ ]
436
+
437
+ # Mock time.sleep to track calls
438
+ sleep_calls: List[float] = []
439
+
440
+ def mock_sleep(seconds: float) -> None:
441
+ sleep_calls.append(seconds)
442
+
443
+ monkeypatch.setattr("time.sleep", mock_sleep)
444
+
445
+ resp = await client.check_result(
446
+ id="xyz", wait_for_completion=True, download_outputs=False
447
+ )
448
+
449
+ assert resp.status == "complete"
450
+ # Should have slept once with default interval (0.5)
451
+ assert len(sleep_calls) == 1
452
+ assert sleep_calls[0] == 0.5
453
+
454
+
455
+ @pytest.mark.asyncio
456
+ async def test_async_check_result_poll_interval_custom(
457
+ mock_async_base_client: AsyncMock, monkeypatch: Any
458
+ ) -> None:
459
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
460
+
461
+ # Set custom poll interval
462
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "2.0")
463
+
464
+ # First calls return queued, then complete
465
+ mock_async_base_client.request.side_effect = [
466
+ DummyResponse(status="queued"),
467
+ DummyResponse(status="complete"),
468
+ ]
469
+
470
+ # Mock time.sleep to track calls
471
+ sleep_calls: List[float] = []
472
+
473
+ def mock_sleep(seconds: float) -> None:
474
+ sleep_calls.append(seconds)
475
+
476
+ monkeypatch.setattr("time.sleep", mock_sleep)
477
+
478
+ resp = await client.check_result(
479
+ id="xyz", wait_for_completion=True, download_outputs=False
480
+ )
481
+
482
+ assert resp.status == "complete"
483
+ # Should have slept once with custom interval (2.0)
484
+ assert len(sleep_calls) == 1
485
+ assert sleep_calls[0] == 2.0
486
+
487
+
488
+ @pytest.mark.asyncio
489
+ async def test_async_check_result_poll_interval_multiple_polls(
490
+ mock_async_base_client: AsyncMock, monkeypatch: Any
491
+ ) -> None:
492
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
493
+
494
+ # Set custom poll interval
495
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "0.3")
496
+
497
+ # Multiple calls return queued before complete
498
+ mock_async_base_client.request.side_effect = [
499
+ DummyResponse(status="queued"),
500
+ DummyResponse(status="queued"),
501
+ DummyResponse(status="queued"),
502
+ DummyResponse(status="complete"),
503
+ ]
504
+
505
+ # Mock time.sleep to track calls
506
+ sleep_calls: List[float] = []
507
+
508
+ def mock_sleep(seconds: float) -> None:
509
+ sleep_calls.append(seconds)
510
+
511
+ monkeypatch.setattr("time.sleep", mock_sleep)
512
+
513
+ resp = await client.check_result(
514
+ id="xyz", wait_for_completion=True, download_outputs=False
515
+ )
516
+
517
+ assert resp.status == "complete"
518
+ # Should have slept 3 times with custom interval (0.3)
519
+ assert len(sleep_calls) == 3
520
+ assert all(sleep_time == 0.3 for sleep_time in sleep_calls)
@@ -40,6 +40,9 @@ res = client.v1.lip_sync.generate(
40
40
  "video_file_path": "/path/to/1234.mp4",
41
41
  "video_source": "file",
42
42
  },
43
+ style={
44
+ "generation_mode": "lite",
45
+ },
43
46
  end_seconds=15.0,
44
47
  start_seconds=0.0,
45
48
  max_fps_limit=12.0,
@@ -63,6 +66,9 @@ res = await client.v1.lip_sync.generate(
63
66
  "video_file_path": "/path/to/1234.mp4",
64
67
  "video_source": "file",
65
68
  },
69
+ style={
70
+ "generation_mode": "lite",
71
+ },
66
72
  end_seconds=15.0,
67
73
  start_seconds=0.0,
68
74
  max_fps_limit=12.0,
@@ -114,6 +120,9 @@ res = client.v1.lip_sync.create(
114
120
  "video_file_path": "api-assets/id/1234.mp4",
115
121
  "video_source": "file",
116
122
  },
123
+ style={
124
+ "generation_mode": "lite",
125
+ },
117
126
  end_seconds=15.0,
118
127
  start_seconds=0.0,
119
128
  max_fps_limit=12.0,
@@ -135,6 +144,9 @@ res = await client.v1.lip_sync.create(
135
144
  "video_file_path": "api-assets/id/1234.mp4",
136
145
  "video_source": "file",
137
146
  },
147
+ style={
148
+ "generation_mode": "lite",
149
+ },
138
150
  end_seconds=15.0,
139
151
  start_seconds=0.0,
140
152
  max_fps_limit=12.0,
@@ -39,6 +39,9 @@ class LipSyncClient:
39
39
  name: typing.Union[
40
40
  typing.Optional[str], type_utils.NotGiven
41
41
  ] = type_utils.NOT_GIVEN,
42
+ style: typing.Union[
43
+ typing.Optional[params.V1LipSyncCreateBodyStyle], type_utils.NotGiven
44
+ ] = type_utils.NOT_GIVEN,
42
45
  width: typing.Union[
43
46
  typing.Optional[int], type_utils.NotGiven
44
47
  ] = type_utils.NOT_GIVEN,
@@ -56,6 +59,7 @@ class LipSyncClient:
56
59
  height: `height` is deprecated and no longer influences the output video's resolution.
57
60
  max_fps_limit: Defines the maximum FPS (frames per second) for the output video. If the input video's FPS is lower than this limit, the output video will retain the input FPS. This is useful for reducing unnecessary frame usage in scenarios where high FPS is not required.
58
61
  name: The name of video. This value is mainly used for your own identification of the video.
62
+ style: Attributes used to dictate the style of the output
59
63
  width: `width` is deprecated and no longer influences the output video's resolution.
60
64
  assets: Provide the assets for lip-sync. For video, The `video_source` field determines whether `video_file_path` or `youtube_url` field is used
61
65
  end_seconds: The end time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.1, and more than the start_seconds.
@@ -108,6 +112,7 @@ class LipSyncClient:
108
112
  height=height,
109
113
  max_fps_limit=max_fps_limit,
110
114
  name=name,
115
+ style=style,
111
116
  width=width,
112
117
  request_options=request_options,
113
118
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: magic_hour
3
- Version: 0.41.0
3
+ Version: 0.42.0
4
4
  Summary: Python SDK for Magic Hour API
5
5
  Requires-Python: >=3.8,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -226,6 +226,7 @@ download_urls = result.downloads
226
226
  ### [v1.ai_voice_generator](magic_hour/resources/v1/ai_voice_generator/README.md)
227
227
 
228
228
  * [create](magic_hour/resources/v1/ai_voice_generator/README.md#create) - AI Voice Generator
229
+ * [generate](magic_hour/resources/v1/ai_voice_generator/README.md#generate) - Ai Voice Generator Generate Workflow
229
230
 
230
231
  ### [v1.animation](magic_hour/resources/v1/animation/README.md)
231
232
 
@@ -234,6 +235,7 @@ download_urls = result.downloads
234
235
 
235
236
  ### [v1.audio_projects](magic_hour/resources/v1/audio_projects/README.md)
236
237
 
238
+ * [check-result](magic_hour/resources/v1/audio_projects/README.md#check-result) - Check results
237
239
  * [delete](magic_hour/resources/v1/audio_projects/README.md#delete) - Delete audio
238
240
  * [get](magic_hour/resources/v1/audio_projects/README.md#get) - Get audio details
239
241
 
@@ -3,7 +3,7 @@ magic_hour/__init__.py,sha256=ypWA4msPy3T-U2PAyRSAXIQc0m2J9aY8wXnfmIupAEc,214
3
3
  magic_hour/client.py,sha256=oHuz0FRKuedtwwPSkFLaSjoEWt_0N57hJ0rEDKbNcFw,1712
4
4
  magic_hour/environment.py,sha256=tAHxEiK1_4L0_MZntbQuQA1Np-kDHZ8M6QbpON1uwsY,535
5
5
  magic_hour/helpers/__init__.py,sha256=pO6sgb41iVe_IN2dApfl2l3BXOWIb4i5pHl_if-oo_E,176
6
- magic_hour/helpers/download.py,sha256=wZR-gddqU2rWzltHN2zqqaHaShHG33yx7BIlUn1jXG8,2212
6
+ magic_hour/helpers/download.py,sha256=_aZmpr9tzqzeyPbvc1Xz95GJhEgOqH1-GHh66Ild57E,2336
7
7
  magic_hour/helpers/logger.py,sha256=Z8ljney670Kc8DaSzlHdH9N8RXmj2_qWPovLUoC0tNY,210
8
8
  magic_hour/resources/v1/README.md,sha256=P5Y3atMFloJotSgJWt5Wtz2z9YnV3B5bcWBu-vppzP8,1793
9
9
  magic_hour/resources/v1/__init__.py,sha256=Aj0sjVcoijjQyieNBxv2_uewPYC2vO2UG-ehoBgCz5E,86
@@ -40,15 +40,16 @@ magic_hour/resources/v1/ai_qr_code_generator/client.py,sha256=rLw0V6cE2AncqM9Bz_
40
40
  magic_hour/resources/v1/ai_talking_photo/README.md,sha256=M33SsFSeGhOVlDKy14qR7ClUD-6qrrMghnwOxgq7vBU,5609
41
41
  magic_hour/resources/v1/ai_talking_photo/__init__.py,sha256=ZTDD_IRBoR7GSdGWCVEK2-LOEsKUdGEHZZvDHa9MOnA,134
42
42
  magic_hour/resources/v1/ai_talking_photo/client.py,sha256=kbFM3peM5NEzUle1miup7Xe2q_HwL7u7kzuD1DkcEBc,12529
43
- magic_hour/resources/v1/ai_voice_generator/README.md,sha256=A8K356cP6Q2Q83OP--qdjpTmCo_TcehvHUBQEkPChXE,1837
43
+ magic_hour/resources/v1/ai_voice_generator/README.md,sha256=gfGlxiXD04a8HutUjMb1GVoEs0D6kxnMNjSJny5y9Zo,3569
44
44
  magic_hour/resources/v1/ai_voice_generator/__init__.py,sha256=Zl1HuxfafN3hzJCCCD_YyCX_y2v4uAihSDAC8GCcE8U,142
45
- magic_hour/resources/v1/ai_voice_generator/client.py,sha256=6XV4PRkRZqqLe3q8OVNO-tGEpsR59QsaNTMlQE3yGGQ,3922
45
+ magic_hour/resources/v1/ai_voice_generator/client.py,sha256=VhrGUhdznSDSBgzLNa7pjqjWC7-cXzYVUyjyGf4c7xo,8749
46
46
  magic_hour/resources/v1/animation/README.md,sha256=ZRyrlI2qh2aYJ-sHF7_8vFMLcQlmDcQEsu78LGVu958,7602
47
47
  magic_hour/resources/v1/animation/__init__.py,sha256=M6KUe6TEZl_DAdyn1HFQ2kHYanZo6xy3mvUdCN264hQ,114
48
48
  magic_hour/resources/v1/animation/client.py,sha256=vVoYF6P-OU83kAXwZyRJngv6C_zoArXXuM6nTPy-Jvw,15447
49
- magic_hour/resources/v1/audio_projects/README.md,sha256=8OXRVddZNw27p9OyHyq-2OEgDdiUZ7gTGILIlrs7QSA,2596
50
- magic_hour/resources/v1/audio_projects/__init__.py,sha256=zPN8qohWZr3d1TXpCLQHfj7jK8blNDOEx7_9K63MG68,130
51
- magic_hour/resources/v1/audio_projects/client.py,sha256=yeOk77fv9YXnk6h1kZJFZE0cMYqsw462IWq8YwcNR7w,5784
49
+ magic_hour/resources/v1/audio_projects/README.md,sha256=pRoIuJvk5BcgdQq4WVQt98WQ0dk8FYFvkLKE-Jue1SM,4375
50
+ magic_hour/resources/v1/audio_projects/__init__.py,sha256=QG5bxr8z40p54-38pawFobEowm_nrx5IURMU4hjrCtI,246
51
+ magic_hour/resources/v1/audio_projects/client.py,sha256=9AIDLuAU6TswVWS7BAnmL2F-PbVz-UcCwy1OjLnOfas,10979
52
+ magic_hour/resources/v1/audio_projects/client_test.py,sha256=n1nMtZgc9TTBKGiiVcMciX3jXHtMs5cAPRrD6s9e6L8,15776
52
53
  magic_hour/resources/v1/auto_subtitle_generator/README.md,sha256=IGi0wXf4T_iBI6fFXVr7enTDKYNCwN3HorDOjQuRyJA,5309
53
54
  magic_hour/resources/v1/auto_subtitle_generator/__init__.py,sha256=dnWFEiSdIl3AwFVprqWHSMzqpeHgZz9wPEMxm7c3Xnc,162
54
55
  magic_hour/resources/v1/auto_subtitle_generator/client.py,sha256=QZREuGcgIVc3hNaBvTfDuBRj0pvnr_uu0YzFkFhsd_s,14803
@@ -79,9 +80,9 @@ magic_hour/resources/v1/image_projects/client_test.py,sha256=5KH4s0vG13SEUkB6pAa
79
80
  magic_hour/resources/v1/image_to_video/README.md,sha256=3_wM8ZtJfZrWMLQnq092v2uIycFvIIjm8tPz7T0A3OY,6273
80
81
  magic_hour/resources/v1/image_to_video/__init__.py,sha256=tY_ABo6evwKQBRSq-M84lNX-pXqmxoozukmrO6NhCgA,126
81
82
  magic_hour/resources/v1/image_to_video/client.py,sha256=fUajB5-kKzT3WC5u7EAeY8_N4k1_r36JmwML4GquQwc,17392
82
- magic_hour/resources/v1/lip_sync/README.md,sha256=2qkWaodE4Au4Z1CU11r7TxxuZdbU1tpeKUr066msmx8,7389
83
+ magic_hour/resources/v1/lip_sync/README.md,sha256=4KSIj9aOz3vgwdMkpe-X6l86j9BvYPl-RhAS51u4bsU,7605
83
84
  magic_hour/resources/v1/lip_sync/__init__.py,sha256=MlKUAoHNSKcuNzVyqNfLnLtD_PsqEn3l1TtVpPC1JqQ,106
84
- magic_hour/resources/v1/lip_sync/client.py,sha256=NbyMKOZ0mUTNODxnWjaBU6y3zRPf772s4rzGnpC1jKE,19529
85
+ magic_hour/resources/v1/lip_sync/client.py,sha256=hFnNps9HSEG6Zi3x56PP5WorbFzm7NnicRiLfR2kZ8o,19769
85
86
  magic_hour/resources/v1/photo_colorizer/README.md,sha256=wql3I3PNIwMZkiCWaQCbMujrtIHW1y4vJ5ce5HX7q0k,3430
86
87
  magic_hour/resources/v1/photo_colorizer/__init__.py,sha256=7rDjkeUzWG5GXw_4RD1XH7Ygy-_0_OUFX99IgE_RVbE,134
87
88
  magic_hour/resources/v1/photo_colorizer/client.py,sha256=mW5VYDmsvU9phtkiY008zarM1CCaWTMTeOTY4zaWDfI,8861
@@ -216,7 +217,7 @@ magic_hour/types/params/v1_video_to_video_create_body.py,sha256=BV52jJcXnOsKaw-M
216
217
  magic_hour/types/params/v1_video_to_video_create_body_assets.py,sha256=XwdoqT1x5ElcWb6qUfmtabNsyaatEsOygB4gJ154y-M,1660
217
218
  magic_hour/types/params/v1_video_to_video_create_body_style.py,sha256=cYiwKRRSJpwHwL_00wxXcXHvym8q1VsT2VBFOPfJNQM,6000
218
219
  magic_hour/types/params/v1_video_to_video_generate_body_assets.py,sha256=j06Y1RKZKwEXW9Zdfhr3Lntx7eDAKpH-k1N1XkKuqPM,915
219
- magic_hour-0.41.0.dist-info/LICENSE,sha256=F3fxj7JXPgB2K0uj8YXRsVss4u-Dgt_-U3V4VXsivNI,1070
220
- magic_hour-0.41.0.dist-info/METADATA,sha256=jGmEEuPpFVOnSSQWc1bGfavWmJ-zc_5tcla3U5rX1Ho,12978
221
- magic_hour-0.41.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
222
- magic_hour-0.41.0.dist-info/RECORD,,
220
+ magic_hour-0.42.0.dist-info/LICENSE,sha256=F3fxj7JXPgB2K0uj8YXRsVss4u-Dgt_-U3V4VXsivNI,1070
221
+ magic_hour-0.42.0.dist-info/METADATA,sha256=Tr3fizT_MQzlJf0YRwTsIc69GnsZt1fRhUPmbp9Vaxo,13189
222
+ magic_hour-0.42.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
223
+ magic_hour-0.42.0.dist-info/RECORD,,