hiapi-seedance 0.1.0__tar.gz
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.
- hiapi_seedance-0.1.0/.gitignore +13 -0
- hiapi_seedance-0.1.0/LICENSE +21 -0
- hiapi_seedance-0.1.0/PKG-INFO +188 -0
- hiapi_seedance-0.1.0/README.md +160 -0
- hiapi_seedance-0.1.0/pyproject.toml +74 -0
- hiapi_seedance-0.1.0/src/hiapi_seedance/__init__.py +22 -0
- hiapi_seedance-0.1.0/src/hiapi_seedance/_version.py +1 -0
- hiapi_seedance-0.1.0/src/hiapi_seedance/client.py +278 -0
- hiapi_seedance-0.1.0/src/hiapi_seedance/errors.py +50 -0
- hiapi_seedance-0.1.0/src/hiapi_seedance/models.py +81 -0
- hiapi_seedance-0.1.0/src/hiapi_seedance/py.typed +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HiAPI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hiapi-seedance
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Seedance 2.5 and Seedance video generation through the HiAPI unified task API.
|
|
5
|
+
Project-URL: Homepage, https://www.hiapi.ai/en?utm_source=github&utm_medium=pyproject&utm_campaign=hiapi-seedance-python
|
|
6
|
+
Project-URL: Documentation, https://docs.hiapi.ai/?utm_source=github&utm_medium=pyproject&utm_campaign=hiapi-seedance-python
|
|
7
|
+
Project-URL: Source, https://github.com/HiAPIAI/hiapi-seedance-python
|
|
8
|
+
Author-email: HiAPI <support@hiapi.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai video generation,hiapi,image to video api,seedance,seedance 2.5,seedance 2.5 api,seedance python sdk,text to video api
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# HiAPI Seedance Python SDK
|
|
30
|
+
|
|
31
|
+
Python SDK for **Seedance 2.5 API**, **Seedance text-to-video**, **Seedance image-to-video**, and Seedance video editing through the HiAPI unified task API.
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/hiapi-seedance/) [](https://www.hiapi.ai/en?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python) [](https://docs.hiapi.ai/?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python)
|
|
34
|
+
|
|
35
|
+
Use this SDK when you want a small Python client focused on AI video generation:
|
|
36
|
+
|
|
37
|
+
- **Seedance 2.5 API access** through HiAPI's `/v1/tasks` endpoint.
|
|
38
|
+
- **Text-to-video** with `client.text_to_video(...)`.
|
|
39
|
+
- **Image-to-video** with `client.image_to_video(...)`.
|
|
40
|
+
- **Video editing** with `client.video_edit(...)`.
|
|
41
|
+
- **Async task polling** built in: submit, poll, and read the output URL.
|
|
42
|
+
- **Zero runtime dependencies**: standard library only.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install hiapi-seedance
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires Python 3.8+.
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from hiapi_seedance import Seedance
|
|
56
|
+
|
|
57
|
+
client = Seedance() # uses HIAPI_API_KEY
|
|
58
|
+
|
|
59
|
+
task = client.text_to_video(
|
|
60
|
+
prompt="A 30-second cinematic one-take product film, golden hour, slow dolly in",
|
|
61
|
+
duration=30,
|
|
62
|
+
aspect_ratio="16:9",
|
|
63
|
+
resolution="1080p",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
print(task.output[0].url)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Set your API key:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export HIAPI_API_KEY="sk-your-hiapi-key"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Get a key at [HiAPI](https://www.hiapi.ai/en/register?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python).
|
|
76
|
+
|
|
77
|
+
## Text-to-Video
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from hiapi_seedance import Seedance
|
|
81
|
+
|
|
82
|
+
client = Seedance(api_key="sk-...")
|
|
83
|
+
|
|
84
|
+
task = client.text_to_video(
|
|
85
|
+
prompt=(
|
|
86
|
+
"A one-take AI video prompt: a glass perfume bottle rotates on wet black "
|
|
87
|
+
"stone while sunrise light moves across the surface, macro texture, no text."
|
|
88
|
+
),
|
|
89
|
+
duration=15,
|
|
90
|
+
aspect_ratio="16:9",
|
|
91
|
+
resolution="1080p",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
print(task.status, task.output[0].url)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Image-to-Video
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
task = client.image_to_video(
|
|
101
|
+
prompt="Animate the reference image with a slow push-in, drifting fabric, and warm rim light.",
|
|
102
|
+
image_urls=["https://example.com/first-frame.png"],
|
|
103
|
+
duration=8,
|
|
104
|
+
aspect_ratio="9:16",
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Video Editing
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
task = client.video_edit(
|
|
112
|
+
prompt="Keep the original camera movement and timing. Add a blue-white energy bow in the actor's hands.",
|
|
113
|
+
video_urls=["https://example.com/source.mp4"],
|
|
114
|
+
image_urls=["https://example.com/prop-reference.png"],
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Return Immediately Instead of Waiting
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
created = client.text_to_video(
|
|
122
|
+
prompt="A fashion film in a desert gallery",
|
|
123
|
+
duration=30,
|
|
124
|
+
wait=False,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
task = client.wait(created.task_id, poll_interval=3, timeout=900)
|
|
128
|
+
print(task.output[0].url)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Raw HiAPI Task Shape
|
|
132
|
+
|
|
133
|
+
The SDK sends requests to:
|
|
134
|
+
|
|
135
|
+
```http
|
|
136
|
+
POST https://api.hiapi.ai/v1/tasks
|
|
137
|
+
Authorization: Bearer $HIAPI_API_KEY
|
|
138
|
+
Content-Type: application/json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"model": "seedance-2-5",
|
|
144
|
+
"input": {
|
|
145
|
+
"prompt": "A cinematic Seedance 2.5 prompt",
|
|
146
|
+
"duration": 30,
|
|
147
|
+
"aspect_ratio": "16:9",
|
|
148
|
+
"resolution": "1080p"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
You can pass additional model fields as keyword arguments:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
task = client.text_to_video(
|
|
157
|
+
prompt="A multilingual typography loop",
|
|
158
|
+
duration=15,
|
|
159
|
+
web_search=False,
|
|
160
|
+
generate_audio=True,
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Prompt Library
|
|
165
|
+
|
|
166
|
+
Need production prompts and preview examples? Use the companion prompt library:
|
|
167
|
+
|
|
168
|
+
- [Seedance 2.5 prompt library](https://github.com/HiAPIAI/awesome-seedance-2-5-prompts)
|
|
169
|
+
- [Seedance 2.0 prompt library](https://github.com/HiAPIAI/awesome-seedance-2-0-prompts)
|
|
170
|
+
|
|
171
|
+
## Model Slug
|
|
172
|
+
|
|
173
|
+
The default model is `seedance-2-5`. You can override it per client or per call:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
client = Seedance(model="seedance-2-5")
|
|
177
|
+
task = client.text_to_video(prompt="...", model="seedance-2-0")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Use the fallback model while waiting for Seedance 2.5 availability on your account.
|
|
181
|
+
|
|
182
|
+
## SEO Keywords
|
|
183
|
+
|
|
184
|
+
Seedance 2.5 API, Seedance Python SDK, Seedance API Python, Seedance text to video API, Seedance image to video API, AI video generation API, text-to-video Python, image-to-video Python, HiAPI Seedance API.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# HiAPI Seedance Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for **Seedance 2.5 API**, **Seedance text-to-video**, **Seedance image-to-video**, and Seedance video editing through the HiAPI unified task API.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/hiapi-seedance/) [](https://www.hiapi.ai/en?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python) [](https://docs.hiapi.ai/?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python)
|
|
6
|
+
|
|
7
|
+
Use this SDK when you want a small Python client focused on AI video generation:
|
|
8
|
+
|
|
9
|
+
- **Seedance 2.5 API access** through HiAPI's `/v1/tasks` endpoint.
|
|
10
|
+
- **Text-to-video** with `client.text_to_video(...)`.
|
|
11
|
+
- **Image-to-video** with `client.image_to_video(...)`.
|
|
12
|
+
- **Video editing** with `client.video_edit(...)`.
|
|
13
|
+
- **Async task polling** built in: submit, poll, and read the output URL.
|
|
14
|
+
- **Zero runtime dependencies**: standard library only.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install hiapi-seedance
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Python 3.8+.
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from hiapi_seedance import Seedance
|
|
28
|
+
|
|
29
|
+
client = Seedance() # uses HIAPI_API_KEY
|
|
30
|
+
|
|
31
|
+
task = client.text_to_video(
|
|
32
|
+
prompt="A 30-second cinematic one-take product film, golden hour, slow dolly in",
|
|
33
|
+
duration=30,
|
|
34
|
+
aspect_ratio="16:9",
|
|
35
|
+
resolution="1080p",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
print(task.output[0].url)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Set your API key:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export HIAPI_API_KEY="sk-your-hiapi-key"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Get a key at [HiAPI](https://www.hiapi.ai/en/register?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python).
|
|
48
|
+
|
|
49
|
+
## Text-to-Video
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from hiapi_seedance import Seedance
|
|
53
|
+
|
|
54
|
+
client = Seedance(api_key="sk-...")
|
|
55
|
+
|
|
56
|
+
task = client.text_to_video(
|
|
57
|
+
prompt=(
|
|
58
|
+
"A one-take AI video prompt: a glass perfume bottle rotates on wet black "
|
|
59
|
+
"stone while sunrise light moves across the surface, macro texture, no text."
|
|
60
|
+
),
|
|
61
|
+
duration=15,
|
|
62
|
+
aspect_ratio="16:9",
|
|
63
|
+
resolution="1080p",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
print(task.status, task.output[0].url)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Image-to-Video
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
task = client.image_to_video(
|
|
73
|
+
prompt="Animate the reference image with a slow push-in, drifting fabric, and warm rim light.",
|
|
74
|
+
image_urls=["https://example.com/first-frame.png"],
|
|
75
|
+
duration=8,
|
|
76
|
+
aspect_ratio="9:16",
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Video Editing
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
task = client.video_edit(
|
|
84
|
+
prompt="Keep the original camera movement and timing. Add a blue-white energy bow in the actor's hands.",
|
|
85
|
+
video_urls=["https://example.com/source.mp4"],
|
|
86
|
+
image_urls=["https://example.com/prop-reference.png"],
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Return Immediately Instead of Waiting
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
created = client.text_to_video(
|
|
94
|
+
prompt="A fashion film in a desert gallery",
|
|
95
|
+
duration=30,
|
|
96
|
+
wait=False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
task = client.wait(created.task_id, poll_interval=3, timeout=900)
|
|
100
|
+
print(task.output[0].url)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Raw HiAPI Task Shape
|
|
104
|
+
|
|
105
|
+
The SDK sends requests to:
|
|
106
|
+
|
|
107
|
+
```http
|
|
108
|
+
POST https://api.hiapi.ai/v1/tasks
|
|
109
|
+
Authorization: Bearer $HIAPI_API_KEY
|
|
110
|
+
Content-Type: application/json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"model": "seedance-2-5",
|
|
116
|
+
"input": {
|
|
117
|
+
"prompt": "A cinematic Seedance 2.5 prompt",
|
|
118
|
+
"duration": 30,
|
|
119
|
+
"aspect_ratio": "16:9",
|
|
120
|
+
"resolution": "1080p"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
You can pass additional model fields as keyword arguments:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
task = client.text_to_video(
|
|
129
|
+
prompt="A multilingual typography loop",
|
|
130
|
+
duration=15,
|
|
131
|
+
web_search=False,
|
|
132
|
+
generate_audio=True,
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Prompt Library
|
|
137
|
+
|
|
138
|
+
Need production prompts and preview examples? Use the companion prompt library:
|
|
139
|
+
|
|
140
|
+
- [Seedance 2.5 prompt library](https://github.com/HiAPIAI/awesome-seedance-2-5-prompts)
|
|
141
|
+
- [Seedance 2.0 prompt library](https://github.com/HiAPIAI/awesome-seedance-2-0-prompts)
|
|
142
|
+
|
|
143
|
+
## Model Slug
|
|
144
|
+
|
|
145
|
+
The default model is `seedance-2-5`. You can override it per client or per call:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
client = Seedance(model="seedance-2-5")
|
|
149
|
+
task = client.text_to_video(prompt="...", model="seedance-2-0")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Use the fallback model while waiting for Seedance 2.5 availability on your account.
|
|
153
|
+
|
|
154
|
+
## SEO Keywords
|
|
155
|
+
|
|
156
|
+
Seedance 2.5 API, Seedance Python SDK, Seedance API Python, Seedance text to video API, Seedance image to video API, AI video generation API, text-to-video Python, image-to-video Python, HiAPI Seedance API.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hiapi-seedance"
|
|
7
|
+
description = "Python SDK for Seedance 2.5 and Seedance video generation through the HiAPI unified task API."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "HiAPI", email = "support@hiapi.ai" }]
|
|
12
|
+
keywords = [
|
|
13
|
+
"hiapi",
|
|
14
|
+
"seedance",
|
|
15
|
+
"seedance 2.5",
|
|
16
|
+
"seedance 2.5 api",
|
|
17
|
+
"seedance python sdk",
|
|
18
|
+
"text to video api",
|
|
19
|
+
"image to video api",
|
|
20
|
+
"ai video generation",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.8",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Typing :: Typed",
|
|
33
|
+
]
|
|
34
|
+
dependencies = []
|
|
35
|
+
dynamic = ["version"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://www.hiapi.ai/en?utm_source=github&utm_medium=pyproject&utm_campaign=hiapi-seedance-python"
|
|
39
|
+
Documentation = "https://docs.hiapi.ai/?utm_source=github&utm_medium=pyproject&utm_campaign=hiapi-seedance-python"
|
|
40
|
+
Source = "https://github.com/HiAPIAI/hiapi-seedance-python"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7",
|
|
45
|
+
"mypy>=1.0",
|
|
46
|
+
"ruff>=0.1",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version]
|
|
50
|
+
path = "src/hiapi_seedance/_version.py"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/hiapi_seedance"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
include = ["src/hiapi_seedance", "README.md", "LICENSE"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = "py38"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.pyupgrade]
|
|
66
|
+
keep-runtime-typing = true
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.10"
|
|
70
|
+
strict = true
|
|
71
|
+
files = ["src/hiapi_seedance"]
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""HiAPI Seedance Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .client import Seedance
|
|
7
|
+
from .errors import APIConnectionError, APIError, PollTimeout, SeedanceError, TaskFailed
|
|
8
|
+
from .models import CreatedTask, Output, Task, TaskError
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"__version__",
|
|
12
|
+
"Seedance",
|
|
13
|
+
"CreatedTask",
|
|
14
|
+
"Output",
|
|
15
|
+
"Task",
|
|
16
|
+
"TaskError",
|
|
17
|
+
"SeedanceError",
|
|
18
|
+
"APIError",
|
|
19
|
+
"APIConnectionError",
|
|
20
|
+
"TaskFailed",
|
|
21
|
+
"PollTimeout",
|
|
22
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Seedance-focused client for HiAPI's unified task API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
from .errors import APIConnectionError, APIError, PollTimeout, TaskFailed
|
|
15
|
+
from .models import CreatedTask, Task
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.hiapi.ai/v1"
|
|
18
|
+
DEFAULT_MODEL = "seedance-2-5"
|
|
19
|
+
DEFAULT_TIMEOUT = 60.0
|
|
20
|
+
DEFAULT_POLL_INTERVAL = 3.0
|
|
21
|
+
DEFAULT_WAIT_TIMEOUT = 900.0
|
|
22
|
+
|
|
23
|
+
UrlList = Union[str, Iterable[str]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Seedance:
|
|
27
|
+
"""Python SDK for Seedance video generation through HiAPI.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: HiAPI API key. Falls back to ``HIAPI_API_KEY``.
|
|
31
|
+
base_url: HiAPI base URL. Both ``https://api.hiapi.ai`` and
|
|
32
|
+
``https://api.hiapi.ai/v1`` are accepted.
|
|
33
|
+
model: Default model slug. Defaults to ``seedance-2-5``.
|
|
34
|
+
timeout: HTTP request timeout in seconds.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
api_key: Optional[str] = None,
|
|
40
|
+
*,
|
|
41
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
42
|
+
model: str = DEFAULT_MODEL,
|
|
43
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
44
|
+
) -> None:
|
|
45
|
+
key = api_key or os.environ.get("HIAPI_API_KEY")
|
|
46
|
+
if not key:
|
|
47
|
+
raise ValueError("missing API key: pass api_key=... or set HIAPI_API_KEY")
|
|
48
|
+
self.api_key = key
|
|
49
|
+
self.base_url = _normalize_base_url(base_url)
|
|
50
|
+
self.model = model
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
|
|
53
|
+
def text_to_video(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
prompt: str,
|
|
57
|
+
duration: Optional[int] = None,
|
|
58
|
+
aspect_ratio: Optional[str] = None,
|
|
59
|
+
resolution: Optional[str] = None,
|
|
60
|
+
generate_audio: Optional[bool] = None,
|
|
61
|
+
model: Optional[str] = None,
|
|
62
|
+
callback_url: Optional[str] = None,
|
|
63
|
+
wait: bool = True,
|
|
64
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
65
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
66
|
+
**extra: Any,
|
|
67
|
+
) -> Union[Task, CreatedTask]:
|
|
68
|
+
"""Create a Seedance text-to-video task.
|
|
69
|
+
|
|
70
|
+
Set ``wait=False`` to return immediately with ``CreatedTask``.
|
|
71
|
+
"""
|
|
72
|
+
input_data = _clean(
|
|
73
|
+
{
|
|
74
|
+
"prompt": prompt,
|
|
75
|
+
"duration": duration,
|
|
76
|
+
"aspect_ratio": aspect_ratio,
|
|
77
|
+
"resolution": resolution,
|
|
78
|
+
"generate_audio": generate_audio,
|
|
79
|
+
**extra,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
return self.create(
|
|
83
|
+
input=input_data,
|
|
84
|
+
model=model,
|
|
85
|
+
callback_url=callback_url,
|
|
86
|
+
wait=wait,
|
|
87
|
+
poll_interval=poll_interval,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def image_to_video(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
prompt: str,
|
|
95
|
+
image_urls: UrlList,
|
|
96
|
+
duration: Optional[int] = None,
|
|
97
|
+
aspect_ratio: Optional[str] = None,
|
|
98
|
+
resolution: Optional[str] = None,
|
|
99
|
+
model: Optional[str] = None,
|
|
100
|
+
callback_url: Optional[str] = None,
|
|
101
|
+
wait: bool = True,
|
|
102
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
103
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
104
|
+
**extra: Any,
|
|
105
|
+
) -> Union[Task, CreatedTask]:
|
|
106
|
+
"""Create a Seedance image-to-video task with reference images."""
|
|
107
|
+
input_data = _clean(
|
|
108
|
+
{
|
|
109
|
+
"prompt": prompt,
|
|
110
|
+
"reference_image_urls": _as_list(image_urls),
|
|
111
|
+
"duration": duration,
|
|
112
|
+
"aspect_ratio": aspect_ratio,
|
|
113
|
+
"resolution": resolution,
|
|
114
|
+
**extra,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
return self.create(
|
|
118
|
+
input=input_data,
|
|
119
|
+
model=model,
|
|
120
|
+
callback_url=callback_url,
|
|
121
|
+
wait=wait,
|
|
122
|
+
poll_interval=poll_interval,
|
|
123
|
+
timeout=timeout,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def video_edit(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
prompt: str,
|
|
130
|
+
video_urls: UrlList,
|
|
131
|
+
image_urls: Optional[UrlList] = None,
|
|
132
|
+
model: Optional[str] = None,
|
|
133
|
+
callback_url: Optional[str] = None,
|
|
134
|
+
wait: bool = True,
|
|
135
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
136
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
137
|
+
**extra: Any,
|
|
138
|
+
) -> Union[Task, CreatedTask]:
|
|
139
|
+
"""Create a Seedance video-edit task with source video references."""
|
|
140
|
+
input_data = _clean(
|
|
141
|
+
{
|
|
142
|
+
"prompt": prompt,
|
|
143
|
+
"reference_video_urls": _as_list(video_urls),
|
|
144
|
+
"reference_image_urls": _as_list(image_urls) if image_urls else None,
|
|
145
|
+
**extra,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
return self.create(
|
|
149
|
+
input=input_data,
|
|
150
|
+
model=model,
|
|
151
|
+
callback_url=callback_url,
|
|
152
|
+
wait=wait,
|
|
153
|
+
poll_interval=poll_interval,
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def create(
|
|
158
|
+
self,
|
|
159
|
+
*,
|
|
160
|
+
input: Dict[str, Any],
|
|
161
|
+
model: Optional[str] = None,
|
|
162
|
+
callback_url: Optional[str] = None,
|
|
163
|
+
wait: bool = False,
|
|
164
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
165
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
166
|
+
) -> Union[Task, CreatedTask]:
|
|
167
|
+
"""Submit a raw HiAPI task for Seedance-compatible inputs."""
|
|
168
|
+
body: Dict[str, Any] = {"model": model or self.model, "input": input}
|
|
169
|
+
if callback_url:
|
|
170
|
+
body["callback"] = {"url": callback_url, "when": "final"}
|
|
171
|
+
env = self._request("POST", "/tasks", body=body)
|
|
172
|
+
created = CreatedTask.from_dict(_data(env))
|
|
173
|
+
if wait:
|
|
174
|
+
return self.wait(created.task_id, poll_interval=poll_interval, timeout=timeout)
|
|
175
|
+
return created
|
|
176
|
+
|
|
177
|
+
def retrieve(self, task_id: str) -> Task:
|
|
178
|
+
"""Fetch a task by id."""
|
|
179
|
+
env = self._request("GET", "/tasks/" + urllib.parse.quote(task_id, safe=""))
|
|
180
|
+
return Task.from_dict(_data(env))
|
|
181
|
+
|
|
182
|
+
def wait(
|
|
183
|
+
self,
|
|
184
|
+
task_id: str,
|
|
185
|
+
*,
|
|
186
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
187
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
188
|
+
) -> Task:
|
|
189
|
+
"""Poll a task until it succeeds, fails, or times out."""
|
|
190
|
+
if poll_interval <= 0:
|
|
191
|
+
raise ValueError("poll_interval must be > 0")
|
|
192
|
+
if timeout < 0:
|
|
193
|
+
raise ValueError("timeout must be >= 0")
|
|
194
|
+
|
|
195
|
+
deadline = time.monotonic() + timeout
|
|
196
|
+
while True:
|
|
197
|
+
task = self.retrieve(task_id)
|
|
198
|
+
if task.status == "success":
|
|
199
|
+
return task
|
|
200
|
+
if task.status == "fail":
|
|
201
|
+
raise TaskFailed(task)
|
|
202
|
+
remaining = deadline - time.monotonic()
|
|
203
|
+
if remaining <= 0:
|
|
204
|
+
raise PollTimeout(task_id, timeout)
|
|
205
|
+
time.sleep(min(poll_interval, remaining))
|
|
206
|
+
|
|
207
|
+
def _request(
|
|
208
|
+
self,
|
|
209
|
+
method: str,
|
|
210
|
+
path: str,
|
|
211
|
+
*,
|
|
212
|
+
body: Optional[Dict[str, Any]] = None,
|
|
213
|
+
) -> Dict[str, Any]:
|
|
214
|
+
url = self.base_url + path
|
|
215
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
216
|
+
headers = {
|
|
217
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
218
|
+
"Accept": "application/json",
|
|
219
|
+
"User-Agent": f"hiapi-seedance/{__version__}",
|
|
220
|
+
}
|
|
221
|
+
if data is not None:
|
|
222
|
+
headers["Content-Type"] = "application/json"
|
|
223
|
+
|
|
224
|
+
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
225
|
+
try:
|
|
226
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
227
|
+
return _decode(response.read(), response.status)
|
|
228
|
+
except urllib.error.HTTPError as exc:
|
|
229
|
+
raw = exc.read()
|
|
230
|
+
raise _api_error(exc.code, raw) from None
|
|
231
|
+
except OSError as exc:
|
|
232
|
+
raise APIConnectionError(str(getattr(exc, "reason", exc))) from exc
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _normalize_base_url(base_url: str) -> str:
|
|
236
|
+
base = base_url.rstrip("/")
|
|
237
|
+
if not base.endswith("/v1"):
|
|
238
|
+
base += "/v1"
|
|
239
|
+
return base
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _decode(raw: bytes, status: int) -> Dict[str, Any]:
|
|
243
|
+
try:
|
|
244
|
+
parsed = json.loads(raw.decode("utf-8")) if raw else {}
|
|
245
|
+
except (UnicodeDecodeError, ValueError) as exc:
|
|
246
|
+
raise APIError(str(exc), status=status, body=raw.decode("utf-8", "replace")) from exc
|
|
247
|
+
if not isinstance(parsed, dict):
|
|
248
|
+
raise APIError("expected JSON object response", status=status, body=str(parsed))
|
|
249
|
+
return parsed
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _api_error(status: int, raw: bytes) -> APIError:
|
|
253
|
+
message = f"HTTP {status}"
|
|
254
|
+
error_code = None
|
|
255
|
+
body = raw.decode("utf-8", "replace") if raw else None
|
|
256
|
+
try:
|
|
257
|
+
parsed = json.loads(raw.decode("utf-8")) if raw else {}
|
|
258
|
+
if isinstance(parsed, dict):
|
|
259
|
+
message = str(parsed.get("message") or message)
|
|
260
|
+
error_code = parsed.get("error_code")
|
|
261
|
+
except (UnicodeDecodeError, ValueError):
|
|
262
|
+
pass
|
|
263
|
+
return APIError(message, status=status, error_code=error_code, body=body)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _data(env: Dict[str, Any]) -> Dict[str, Any]:
|
|
267
|
+
data = env.get("data")
|
|
268
|
+
return data if isinstance(data, dict) else {}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _clean(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
272
|
+
return {key: value for key, value in data.items() if value is not None}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _as_list(value: UrlList) -> List[str]:
|
|
276
|
+
if isinstance(value, str):
|
|
277
|
+
return [value]
|
|
278
|
+
return [str(item) for item in value]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Exception hierarchy for hiapi-seedance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SeedanceError(Exception):
|
|
9
|
+
"""Base class for all SDK errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class APIError(SeedanceError):
|
|
13
|
+
"""The API returned a non-2xx response."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
*,
|
|
19
|
+
status: int,
|
|
20
|
+
error_code: Optional[str] = None,
|
|
21
|
+
body: Optional[str] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.status = status
|
|
25
|
+
self.error_code = error_code
|
|
26
|
+
self.body = body
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class APIConnectionError(SeedanceError):
|
|
30
|
+
"""A network error prevented the request from completing."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TaskFailed(SeedanceError):
|
|
34
|
+
"""A polled task reached terminal status=fail."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, task: object) -> None:
|
|
37
|
+
err = getattr(task, "error", None)
|
|
38
|
+
self.code = getattr(err, "code", None)
|
|
39
|
+
self.message = getattr(err, "message", None) or "task failed"
|
|
40
|
+
self.task = task
|
|
41
|
+
super().__init__(f"task {getattr(task, 'task_id', '?')} failed: {self.message}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PollTimeout(SeedanceError):
|
|
45
|
+
"""The task did not finish within the client-side timeout."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, task_id: str, timeout: float) -> None:
|
|
48
|
+
self.task_id = task_id
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
super().__init__(f"task {task_id} did not finish within {timeout:g}s")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Typed response models for the HiAPI Seedance SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
TERMINAL_STATUSES = frozenset({"success", "fail"})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Output:
|
|
13
|
+
url: str
|
|
14
|
+
type: str = ""
|
|
15
|
+
expire_at: Optional[int] = None
|
|
16
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_dict(cls, data: Dict[str, Any]) -> Output:
|
|
20
|
+
return cls(
|
|
21
|
+
url=str(data.get("url", "")),
|
|
22
|
+
type=str(data.get("type", "")),
|
|
23
|
+
expire_at=data.get("expireAt"),
|
|
24
|
+
raw=data,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TaskError:
|
|
30
|
+
code: Optional[str] = None
|
|
31
|
+
message: Optional[str] = None
|
|
32
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: Dict[str, Any]) -> TaskError:
|
|
36
|
+
return cls(code=data.get("code"), message=data.get("message"), raw=data)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class CreatedTask:
|
|
41
|
+
task_id: str
|
|
42
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, data: Dict[str, Any]) -> CreatedTask:
|
|
46
|
+
return cls(task_id=str(data.get("taskId", "")), raw=data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Task:
|
|
51
|
+
task_id: str
|
|
52
|
+
model: str
|
|
53
|
+
status: str
|
|
54
|
+
output: List[Output] = field(default_factory=list)
|
|
55
|
+
error: Optional[TaskError] = None
|
|
56
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_terminal(self) -> bool:
|
|
60
|
+
return self.status in TERMINAL_STATUSES
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def succeeded(self) -> bool:
|
|
64
|
+
return self.status == "success"
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_dict(cls, data: Dict[str, Any]) -> Task:
|
|
68
|
+
outputs = [
|
|
69
|
+
Output.from_dict(item)
|
|
70
|
+
for item in (data.get("output") or [])
|
|
71
|
+
if isinstance(item, dict)
|
|
72
|
+
]
|
|
73
|
+
err = data.get("error")
|
|
74
|
+
return cls(
|
|
75
|
+
task_id=str(data.get("taskId", "")),
|
|
76
|
+
model=str(data.get("model", "")),
|
|
77
|
+
status=str(data.get("status", "")),
|
|
78
|
+
output=outputs,
|
|
79
|
+
error=TaskError.from_dict(err) if isinstance(err, dict) else None,
|
|
80
|
+
raw=data,
|
|
81
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|