runapi-omnihuman 0.1.1__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.
- runapi_omnihuman-0.1.1/PKG-INFO +66 -0
- runapi_omnihuman-0.1.1/README.md +51 -0
- runapi_omnihuman-0.1.1/pyproject.toml +32 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/__init__.py +24 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/client.py +33 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/contract_gen.py +45 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/py.typed +0 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/resources/__init__.py +8 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/resources/audio_to_video.py +39 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/resources/human_identification.py +40 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/resources/subject_detection.py +40 -0
- runapi_omnihuman-0.1.1/src/runapi/omnihuman/types.py +61 -0
- runapi_omnihuman-0.1.1/tests/test_client.py +214 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-omnihuman
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: RunAPI OmniHuman SDK for audio-to-video and image helper workflows in JavaScript, Python, Ruby, Go, Java, and PHP
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/omnihuman
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-omnihuman
|
|
7
|
+
Project-URL: Source, https://github.com/runapi-ai/omnihuman-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/runapi-ai/omnihuman-sdk/issues
|
|
9
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: api,audio-to-video,golang,gradle,java,maven,omnihuman,omnihuman-api,python,ruby,runapi,runapi-ai,sdk,typescript,video-api,video-generation
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: runapi-core>=0.1.4
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# OmniHuman Python SDK for RunAPI
|
|
17
|
+
|
|
18
|
+
The OmniHuman Python SDK is the language-specific package for OmniHuman on RunAPI. Use this package for audio-driven talking-head video generation, human identification, and subject-mask detection when your application needs request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
19
|
+
|
|
20
|
+
This README is the Python package guide inside the public `omnihuman-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/omnihuman; for API reference, use https://runapi.ai/docs#omnihuman; for SDK docs, use https://runapi.ai/docs#sdk-omnihuman.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install runapi-omnihuman
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from runapi.omnihuman import OmnihumanClient
|
|
32
|
+
|
|
33
|
+
client = OmnihumanClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
34
|
+
|
|
35
|
+
task = client.audio_to_video.create(
|
|
36
|
+
model="omnihuman-1.5",
|
|
37
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
38
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/voice.mp3",
|
|
39
|
+
output_resolution="720p",
|
|
40
|
+
)
|
|
41
|
+
status = client.audio_to_video.get(task.id)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion. In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
45
|
+
|
|
46
|
+
RunAPI-generated file URLs are temporary. Download and store generated videos, masks, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
47
|
+
|
|
48
|
+
## Language notes
|
|
49
|
+
|
|
50
|
+
Pass parameters as keyword arguments and catch the `runapi.omnihuman` error classes when building video jobs or scripts. The available resources are `audio_to_video`, `human_identification`, and `subject_detection`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
51
|
+
|
|
52
|
+
## Links
|
|
53
|
+
|
|
54
|
+
- Model page: https://runapi.ai/models/omnihuman
|
|
55
|
+
- SDK docs: https://runapi.ai/docs#sdk-omnihuman
|
|
56
|
+
- Product docs: https://runapi.ai/docs#omnihuman
|
|
57
|
+
- Pricing and rate limits: https://runapi.ai/models/omnihuman/1.5
|
|
58
|
+
- Human identification: https://runapi.ai/models/omnihuman/1.5-human-identification
|
|
59
|
+
- Subject detection: https://runapi.ai/models/omnihuman/1.5-subject-detection
|
|
60
|
+
- Provider comparison: https://runapi.ai/providers/bytedance
|
|
61
|
+
- Full catalog: https://runapi.ai/models
|
|
62
|
+
- Repository: https://github.com/runapi-ai/omnihuman-sdk
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# OmniHuman Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The OmniHuman Python SDK is the language-specific package for OmniHuman on RunAPI. Use this package for audio-driven talking-head video generation, human identification, and subject-mask detection when your application needs request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
4
|
+
|
|
5
|
+
This README is the Python package guide inside the public `omnihuman-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/omnihuman; for API reference, use https://runapi.ai/docs#omnihuman; for SDK docs, use https://runapi.ai/docs#sdk-omnihuman.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install runapi-omnihuman
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from runapi.omnihuman import OmnihumanClient
|
|
17
|
+
|
|
18
|
+
client = OmnihumanClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
19
|
+
|
|
20
|
+
task = client.audio_to_video.create(
|
|
21
|
+
model="omnihuman-1.5",
|
|
22
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
23
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/voice.mp3",
|
|
24
|
+
output_resolution="720p",
|
|
25
|
+
)
|
|
26
|
+
status = client.audio_to_video.get(task.id)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion. In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
30
|
+
|
|
31
|
+
RunAPI-generated file URLs are temporary. Download and store generated videos, masks, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
32
|
+
|
|
33
|
+
## Language notes
|
|
34
|
+
|
|
35
|
+
Pass parameters as keyword arguments and catch the `runapi.omnihuman` error classes when building video jobs or scripts. The available resources are `audio_to_video`, `human_identification`, and `subject_detection`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
36
|
+
|
|
37
|
+
## Links
|
|
38
|
+
|
|
39
|
+
- Model page: https://runapi.ai/models/omnihuman
|
|
40
|
+
- SDK docs: https://runapi.ai/docs#sdk-omnihuman
|
|
41
|
+
- Product docs: https://runapi.ai/docs#omnihuman
|
|
42
|
+
- Pricing and rate limits: https://runapi.ai/models/omnihuman/1.5
|
|
43
|
+
- Human identification: https://runapi.ai/models/omnihuman/1.5-human-identification
|
|
44
|
+
- Subject detection: https://runapi.ai/models/omnihuman/1.5-subject-detection
|
|
45
|
+
- Provider comparison: https://runapi.ai/providers/bytedance
|
|
46
|
+
- Full catalog: https://runapi.ai/models
|
|
47
|
+
- Repository: https://github.com/runapi-ai/omnihuman-sdk
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-omnihuman"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "RunAPI OmniHuman SDK for audio-to-video and image helper workflows in JavaScript, Python, Ruby, Go, Java, and PHP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
|
|
13
|
+
keywords = ["runapi", "runapi-ai", "omnihuman", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "maven", "gradle", "video-generation", "audio-to-video", "video-api", "omnihuman-api"]
|
|
14
|
+
dependencies = ["runapi-core>=0.1.4"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/omnihuman"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-omnihuman"
|
|
19
|
+
Source = "https://github.com/runapi-ai/omnihuman-sdk"
|
|
20
|
+
Issues = "https://github.com/runapi-ai/omnihuman-sdk/issues"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/runapi"]
|
|
24
|
+
|
|
25
|
+
[tool.uv]
|
|
26
|
+
package = true
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=8"]
|
|
30
|
+
|
|
31
|
+
[tool.runapi]
|
|
32
|
+
slug = "omnihuman"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""OmniHuman client for RunAPI."""
|
|
2
|
+
|
|
3
|
+
from runapi.core import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
InsufficientCreditsError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
TaskFailedError,
|
|
9
|
+
TaskTimeoutError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .client import OmnihumanClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"OmnihumanClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""OmniHuman client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import ClientOptions, HttpClient, resolve_api_key
|
|
8
|
+
|
|
9
|
+
from .resources.audio_to_video import AudioToVideo
|
|
10
|
+
from .resources.human_identification import HumanIdentification
|
|
11
|
+
from .resources.subject_detection import SubjectDetection
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OmnihumanClient:
|
|
15
|
+
"""OmniHuman audio-to-video and helper endpoint client.
|
|
16
|
+
|
|
17
|
+
Example::
|
|
18
|
+
|
|
19
|
+
client = OmnihumanClient(api_key="sk-...")
|
|
20
|
+
result = client.audio_to_video.run(
|
|
21
|
+
model="omnihuman-1.5",
|
|
22
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
23
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/voice.mp3",
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
28
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
29
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
30
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
31
|
+
self.audio_to_video = AudioToVideo(http)
|
|
32
|
+
self.human_identification = HumanIdentification(http)
|
|
33
|
+
self.subject_detection = SubjectDetection(http)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
CONTRACT = {
|
|
2
|
+
"audio-to-video": {
|
|
3
|
+
"models": ["omnihuman-1.5"],
|
|
4
|
+
"fields_by_model": {
|
|
5
|
+
"omnihuman-1.5": {
|
|
6
|
+
"output_resolution": {
|
|
7
|
+
"enum": ["720p", "1080p"]
|
|
8
|
+
},
|
|
9
|
+
"prompt": {
|
|
10
|
+
"max": 1000,
|
|
11
|
+
"length": True
|
|
12
|
+
},
|
|
13
|
+
"seed": {
|
|
14
|
+
"type": "integer"
|
|
15
|
+
},
|
|
16
|
+
"source_audio_url": {
|
|
17
|
+
"required": True
|
|
18
|
+
},
|
|
19
|
+
"source_image_url": {
|
|
20
|
+
"required": True
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"human-identification": {
|
|
26
|
+
"models": ["omnihuman-1.5-human-identification"],
|
|
27
|
+
"fields_by_model": {
|
|
28
|
+
"omnihuman-1.5-human-identification": {
|
|
29
|
+
"source_image_url": {
|
|
30
|
+
"required": True
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"subject-detection": {
|
|
36
|
+
"models": ["omnihuman-1.5-subject-detection"],
|
|
37
|
+
"fields_by_model": {
|
|
38
|
+
"omnihuman-1.5-subject-detection": {
|
|
39
|
+
"source_image_url": {
|
|
40
|
+
"required": True
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .audio_to_video import AudioToVideo
|
|
2
|
+
|
|
3
|
+
__all__ = ["AudioToVideo"]
|
|
4
|
+
from .audio_to_video import AudioToVideo
|
|
5
|
+
from .human_identification import HumanIdentification
|
|
6
|
+
from .subject_detection import SubjectDetection
|
|
7
|
+
|
|
8
|
+
__all__ = ["AudioToVideo", "HumanIdentification", "SubjectDetection"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""OmniHuman audio-to-video resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
AudioToVideoResponse,
|
|
12
|
+
CompletedAudioToVideoResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
class AudioToVideo(Resource):
|
|
16
|
+
"""Generate a talking-head video from a source image and audio track."""
|
|
17
|
+
|
|
18
|
+
ENDPOINT = "/api/v1/omnihuman/audio_to_video"
|
|
19
|
+
|
|
20
|
+
RESPONSE_CLASS = AudioToVideoResponse
|
|
21
|
+
COMPLETED_RESPONSE_CLASS = CompletedAudioToVideoResponse
|
|
22
|
+
|
|
23
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
24
|
+
"""Create a task and poll until it completes."""
|
|
25
|
+
task = self.create(options=options, **params)
|
|
26
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
27
|
+
|
|
28
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
29
|
+
"""Create an audio-to-video task and return immediately with an ``id``."""
|
|
30
|
+
compacted = self._compact_params(params)
|
|
31
|
+
self._validate_params(compacted)
|
|
32
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
33
|
+
|
|
34
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
35
|
+
"""Fetch the current status of an audio-to-video task."""
|
|
36
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
37
|
+
|
|
38
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
39
|
+
self._validate_contract(CONTRACT["audio-to-video"], params)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""OmniHuman human-identification resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedHumanIdentificationResponse,
|
|
12
|
+
HumanIdentificationResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HumanIdentification(Resource):
|
|
17
|
+
"""Identify human regions in a source image."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/omnihuman/human_identification"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = HumanIdentificationResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedHumanIdentificationResponse
|
|
23
|
+
|
|
24
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
25
|
+
"""Create a task and poll until it completes."""
|
|
26
|
+
task = self.create(options=options, **params)
|
|
27
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
28
|
+
|
|
29
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
30
|
+
"""Create a human-identification task and return immediately with an ``id``."""
|
|
31
|
+
compacted = self._compact_params(params)
|
|
32
|
+
self._validate_params(compacted)
|
|
33
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
34
|
+
|
|
35
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
36
|
+
"""Fetch the current status of a human-identification task."""
|
|
37
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
38
|
+
|
|
39
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
40
|
+
self._validate_contract(CONTRACT["human-identification"], params)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""OmniHuman subject-detection resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedSubjectDetectionResponse,
|
|
12
|
+
SubjectDetectionResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SubjectDetection(Resource):
|
|
17
|
+
"""Detect subject masks in a source image."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/omnihuman/subject_detection"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = SubjectDetectionResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedSubjectDetectionResponse
|
|
23
|
+
|
|
24
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
25
|
+
"""Create a task and poll until it completes."""
|
|
26
|
+
task = self.create(options=options, **params)
|
|
27
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
28
|
+
|
|
29
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
30
|
+
"""Create a subject-detection task and return immediately with an ``id``."""
|
|
31
|
+
compacted = self._compact_params(params)
|
|
32
|
+
self._validate_params(compacted)
|
|
33
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
34
|
+
|
|
35
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
36
|
+
"""Fetch the current status of a subject-detection task."""
|
|
37
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
38
|
+
|
|
39
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
40
|
+
self._validate_contract(CONTRACT["subject-detection"], params)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""OmniHuman response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Video(BaseModel):
|
|
9
|
+
url = optional(str)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Mask(BaseModel):
|
|
13
|
+
url = optional(str)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AudioToVideoResponse(TaskResponse):
|
|
17
|
+
"""Response for an audio-to-video task."""
|
|
18
|
+
|
|
19
|
+
id = required(str)
|
|
20
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
21
|
+
videos = optional([lambda: Video])
|
|
22
|
+
error = optional(str)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CompletedAudioToVideoResponse(AudioToVideoResponse):
|
|
26
|
+
"""Returned by ``audio_to_video.run()`` once polling observes completion.
|
|
27
|
+
|
|
28
|
+
``videos`` is required so callers never have to null-check it on success.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
videos = required([lambda: Video])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HumanIdentificationResponse(TaskResponse):
|
|
35
|
+
"""Response for a human-identification helper task."""
|
|
36
|
+
|
|
37
|
+
id = required(str)
|
|
38
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
39
|
+
subject_status = optional(int)
|
|
40
|
+
error = optional(str)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CompletedHumanIdentificationResponse(HumanIdentificationResponse):
|
|
44
|
+
"""Returned by ``human_identification.run()`` once polling observes completion."""
|
|
45
|
+
|
|
46
|
+
subject_status = required(int)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SubjectDetectionResponse(TaskResponse):
|
|
50
|
+
"""Response for a subject-detection helper task."""
|
|
51
|
+
|
|
52
|
+
id = required(str)
|
|
53
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
54
|
+
masks = optional([lambda: Mask])
|
|
55
|
+
error = optional(str)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CompletedSubjectDetectionResponse(SubjectDetectionResponse):
|
|
59
|
+
"""Returned by ``subject_detection.run()`` once polling observes completion."""
|
|
60
|
+
|
|
61
|
+
masks = required([lambda: Mask])
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.omnihuman import OmnihumanClient
|
|
6
|
+
from runapi.omnihuman.resources.audio_to_video import AudioToVideo
|
|
7
|
+
from runapi.omnihuman.resources.human_identification import HumanIdentification
|
|
8
|
+
from runapi.omnihuman.resources.subject_detection import SubjectDetection
|
|
9
|
+
from runapi.omnihuman.types import (
|
|
10
|
+
AudioToVideoResponse,
|
|
11
|
+
CompletedAudioToVideoResponse,
|
|
12
|
+
CompletedHumanIdentificationResponse,
|
|
13
|
+
CompletedSubjectDetectionResponse,
|
|
14
|
+
HumanIdentificationResponse,
|
|
15
|
+
SubjectDetectionResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeHttp:
|
|
20
|
+
"""Records (method, path, body) and replays preset responses by call order."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *responses):
|
|
23
|
+
self._responses = list(responses)
|
|
24
|
+
self.calls = []
|
|
25
|
+
|
|
26
|
+
def request(self, method, path, body=None, options=None):
|
|
27
|
+
self.calls.append((method, path, body))
|
|
28
|
+
if self._responses:
|
|
29
|
+
return self._responses.pop(0)
|
|
30
|
+
return {"id": "task_1", "status": "pending"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture(autouse=True)
|
|
34
|
+
def reset_config(monkeypatch):
|
|
35
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
36
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
37
|
+
yield
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
VALID_AUDIO_PARAMS = dict(
|
|
41
|
+
model="omnihuman-1.5",
|
|
42
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
43
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/voice.mp3",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_accepts_api_key_parameter():
|
|
48
|
+
assert isinstance(
|
|
49
|
+
OmnihumanClient(api_key="param-key", http_client=FakeHttp()), OmnihumanClient
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_falls_back_to_global(monkeypatch):
|
|
54
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
55
|
+
assert isinstance(OmnihumanClient(http_client=FakeHttp()), OmnihumanClient)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_falls_back_to_env(monkeypatch):
|
|
59
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
60
|
+
assert isinstance(OmnihumanClient(http_client=FakeHttp()), OmnihumanClient)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_raises_without_api_key():
|
|
64
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
65
|
+
OmnihumanClient()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_exposes_resource_accessors():
|
|
69
|
+
client = OmnihumanClient(api_key="k", http_client=FakeHttp())
|
|
70
|
+
assert isinstance(client.audio_to_video, AudioToVideo)
|
|
71
|
+
assert isinstance(client.human_identification, HumanIdentification)
|
|
72
|
+
assert isinstance(client.subject_detection, SubjectDetection)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_audio_to_video_create_posts_compacted_body():
|
|
76
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
77
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
78
|
+
result = client.audio_to_video.create(
|
|
79
|
+
**VALID_AUDIO_PARAMS,
|
|
80
|
+
prompt="A presenter speaks naturally to camera",
|
|
81
|
+
output_resolution="720p",
|
|
82
|
+
enable_fast_mode=True,
|
|
83
|
+
seed=None,
|
|
84
|
+
)
|
|
85
|
+
assert fake.calls == [
|
|
86
|
+
(
|
|
87
|
+
"post",
|
|
88
|
+
"/api/v1/omnihuman/audio_to_video",
|
|
89
|
+
{
|
|
90
|
+
"model": "omnihuman-1.5",
|
|
91
|
+
"source_image_url": "https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
92
|
+
"source_audio_url": "https://cdn.runapi.ai/public/samples/voice.mp3",
|
|
93
|
+
"prompt": "A presenter speaks naturally to camera",
|
|
94
|
+
"output_resolution": "720p",
|
|
95
|
+
"enable_fast_mode": True,
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
assert isinstance(result, AudioToVideoResponse)
|
|
100
|
+
assert result.id == "t1"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_audio_to_video_get_fetches_by_id():
|
|
104
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
105
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
106
|
+
client.audio_to_video.get("t1")
|
|
107
|
+
assert fake.calls == [("get", "/api/v1/omnihuman/audio_to_video/t1", None)]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_audio_to_video_run_polls_and_narrows_completed_type():
|
|
111
|
+
fake = FakeHttp(
|
|
112
|
+
{"id": "t1", "status": "pending"},
|
|
113
|
+
{"id": "t1", "status": "completed", "videos": [{"url": "https://x/y.mp4"}]},
|
|
114
|
+
)
|
|
115
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
116
|
+
result = client.audio_to_video.run(**VALID_AUDIO_PARAMS)
|
|
117
|
+
|
|
118
|
+
assert isinstance(result, CompletedAudioToVideoResponse)
|
|
119
|
+
assert result.videos[0].url == "https://x/y.mp4"
|
|
120
|
+
assert [call[0] for call in fake.calls] == ["post", "get"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_human_identification_create_and_get():
|
|
124
|
+
fake = FakeHttp(
|
|
125
|
+
{"id": "human_1", "status": "pending"},
|
|
126
|
+
{"id": "human_1", "status": "completed", "subject_status": 1},
|
|
127
|
+
)
|
|
128
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
129
|
+
task = client.human_identification.create(
|
|
130
|
+
model="omnihuman-1.5-human-identification",
|
|
131
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
132
|
+
)
|
|
133
|
+
result = client.human_identification.get(task.id)
|
|
134
|
+
|
|
135
|
+
assert fake.calls[0] == (
|
|
136
|
+
"post",
|
|
137
|
+
"/api/v1/omnihuman/human_identification",
|
|
138
|
+
{
|
|
139
|
+
"model": "omnihuman-1.5-human-identification",
|
|
140
|
+
"source_image_url": "https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
assert fake.calls[1] == ("get", "/api/v1/omnihuman/human_identification/human_1", None)
|
|
144
|
+
assert isinstance(result, HumanIdentificationResponse)
|
|
145
|
+
assert result.subject_status == 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_human_identification_run_polls_and_narrows_completed_type():
|
|
149
|
+
fake = FakeHttp(
|
|
150
|
+
{"id": "human_1", "status": "pending"},
|
|
151
|
+
{"id": "human_1", "status": "completed", "subject_status": 1},
|
|
152
|
+
)
|
|
153
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
154
|
+
result = client.human_identification.run(
|
|
155
|
+
model="omnihuman-1.5-human-identification",
|
|
156
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
assert isinstance(result, CompletedHumanIdentificationResponse)
|
|
160
|
+
assert result.subject_status == 1
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_subject_detection_create_and_get():
|
|
164
|
+
fake = FakeHttp(
|
|
165
|
+
{"id": "mask_1", "status": "pending"},
|
|
166
|
+
{"id": "mask_1", "status": "completed", "masks": [{"url": "https://x/mask.png"}]},
|
|
167
|
+
)
|
|
168
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
169
|
+
task = client.subject_detection.create(
|
|
170
|
+
model="omnihuman-1.5-subject-detection",
|
|
171
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
172
|
+
)
|
|
173
|
+
result = client.subject_detection.get(task.id)
|
|
174
|
+
|
|
175
|
+
assert fake.calls[0] == (
|
|
176
|
+
"post",
|
|
177
|
+
"/api/v1/omnihuman/subject_detection",
|
|
178
|
+
{
|
|
179
|
+
"model": "omnihuman-1.5-subject-detection",
|
|
180
|
+
"source_image_url": "https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
assert fake.calls[1] == ("get", "/api/v1/omnihuman/subject_detection/mask_1", None)
|
|
184
|
+
assert isinstance(result, SubjectDetectionResponse)
|
|
185
|
+
assert result.masks[0].url == "https://x/mask.png"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_subject_detection_run_polls_and_narrows_completed_type():
|
|
189
|
+
fake = FakeHttp(
|
|
190
|
+
{"id": "mask_1", "status": "pending"},
|
|
191
|
+
{"id": "mask_1", "status": "completed", "masks": [{"url": "https://x/mask.png"}]},
|
|
192
|
+
)
|
|
193
|
+
client = OmnihumanClient(api_key="k", http_client=fake)
|
|
194
|
+
result = client.subject_detection.run(
|
|
195
|
+
model="omnihuman-1.5-subject-detection",
|
|
196
|
+
source_image_url="https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
assert isinstance(result, CompletedSubjectDetectionResponse)
|
|
200
|
+
assert result.masks[0].url == "https://x/mask.png"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_audio_to_video_requires_model():
|
|
204
|
+
client = OmnihumanClient(api_key="k", http_client=FakeHttp())
|
|
205
|
+
params = {k: v for k, v in VALID_AUDIO_PARAMS.items() if k != "model"}
|
|
206
|
+
with pytest.raises(ValidationError, match="model must be one of: omnihuman-1.5"):
|
|
207
|
+
client.audio_to_video.create(**params)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_audio_to_video_rejects_invalid_resolution():
|
|
211
|
+
client = OmnihumanClient(api_key="k", http_client=FakeHttp())
|
|
212
|
+
params = {**VALID_AUDIO_PARAMS, "output_resolution": "480p"}
|
|
213
|
+
with pytest.raises(ValidationError, match="output_resolution must be one of: 720p, 1080p"):
|
|
214
|
+
client.audio_to_video.create(**params)
|