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.
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .mypy_cache/
6
+ .venv/
7
+ venv/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .env
12
+ .env.*
13
+ !.env.example
@@ -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
+ [![PyPI](https://img.shields.io/badge/pip-hiapi--seedance-f97316)](https://pypi.org/project/hiapi-seedance/) [![HiAPI](https://img.shields.io/badge/HiAPI-One%20API%2C%20All%20AI%20Models-111827)](https://www.hiapi.ai/en?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python) [![Docs](https://img.shields.io/badge/API%20Docs-docs.hiapi.ai-111827)](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
+ [![PyPI](https://img.shields.io/badge/pip-hiapi--seedance-f97316)](https://pypi.org/project/hiapi-seedance/) [![HiAPI](https://img.shields.io/badge/HiAPI-One%20API%2C%20All%20AI%20Models-111827)](https://www.hiapi.ai/en?utm_source=github&utm_medium=readme&utm_campaign=hiapi-seedance-python) [![Docs](https://img.shields.io/badge/API%20Docs-docs.hiapi.ai-111827)](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
+ )