tunova 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.
- tunova-0.1.0/LICENSE +21 -0
- tunova-0.1.0/PKG-INFO +112 -0
- tunova-0.1.0/README.md +69 -0
- tunova-0.1.0/pyproject.toml +34 -0
- tunova-0.1.0/python/tunova.egg-info/PKG-INFO +112 -0
- tunova-0.1.0/python/tunova.egg-info/SOURCES.txt +8 -0
- tunova-0.1.0/python/tunova.egg-info/dependency_links.txt +1 -0
- tunova-0.1.0/python/tunova.egg-info/top_level.txt +1 -0
- tunova-0.1.0/python/tunova.py +159 -0
- tunova-0.1.0/setup.cfg +4 -0
tunova-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tunova
|
|
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.
|
tunova-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunova
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tiny zero-dependency Python client for the Tunova Suno music API (v5.5) — async, billed only on success.
|
|
5
|
+
Author: Tunova
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Tunova
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://tunova.ai
|
|
29
|
+
Project-URL: Documentation, https://api.tunova.ai/docs
|
|
30
|
+
Project-URL: Source, https://github.com/erliona/tunova-sdk
|
|
31
|
+
Keywords: suno,suno-api,music,ai,mcp,api-client
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
38
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
39
|
+
Requires-Python: >=3.8
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Tunova SDK
|
|
45
|
+
|
|
46
|
+
Tiny, **zero-dependency** clients + MCP manifest for the [Tunova](https://tunova.ai) music API —
|
|
47
|
+
generate music with Suno (v5.5) over a simple REST or MCP interface.
|
|
48
|
+
Generation is async and **billed only on success**: a failed render refunds itself.
|
|
49
|
+
|
|
50
|
+
- **Python** → [`python/tunova.py`](python/tunova.py) — stdlib only, Python 3.8+.
|
|
51
|
+
- **Node / TypeScript** → [`node/tunova.ts`](node/tunova.ts) — Node 18+ (`fetch` built in).
|
|
52
|
+
- **MCP** → [`server.json`](server.json) — hosted Streamable-HTTP server at `https://api.tunova.ai/mcp`.
|
|
53
|
+
|
|
54
|
+
Get a key (50 free tokens, no card) at **<https://tunova.ai>**. Full reference:
|
|
55
|
+
<https://api.tunova.ai/docs> · live status: <https://tunova.ai/status>.
|
|
56
|
+
|
|
57
|
+
> Tunova is an independent service, not affiliated with or endorsed by Suno. Tracks come from paid
|
|
58
|
+
> Suno plans; review Suno's terms for your use case.
|
|
59
|
+
|
|
60
|
+
## Python
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install tunova
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from tunova import Tunova
|
|
68
|
+
|
|
69
|
+
t = Tunova("sk_live_…")
|
|
70
|
+
job = t.generate("warm lo-fi piano to study to", model="v5.5")
|
|
71
|
+
print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Prefer no install? `python/tunova.py` is stdlib-only — vendor the single file and `import` it.
|
|
75
|
+
|
|
76
|
+
## Node / TypeScript
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm i tunova
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Tunova } from "tunova";
|
|
84
|
+
|
|
85
|
+
const t = new Tunova(process.env.TUNOVA_API_KEY!);
|
|
86
|
+
const job = await t.generate("warm lo-fi piano to study to", { model: "v5.5" });
|
|
87
|
+
console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Both `generate()` calls submit a job and poll until the track is delivered. Prefer fire-and-forget?
|
|
91
|
+
Use `submit()` with a `callback_url` and take an HMAC-signed webhook instead.
|
|
92
|
+
|
|
93
|
+
## MCP — give an AI agent the power to make music
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
claude mcp add --transport http tunova https://api.tunova.ai/mcp \
|
|
97
|
+
--header "X-API-Key: sk_live_…"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Tools: `generate_song` · `wait_for_song` · `check_song`. Same API key, same billed-on-success rule.
|
|
101
|
+
|
|
102
|
+
## Verifying webhooks
|
|
103
|
+
|
|
104
|
+
If you pass `callback_url`, Tunova POSTs the terminal job with
|
|
105
|
+
`X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<rawBody>`, keyed with your
|
|
106
|
+
`whsec_…` secret (view/rotate it on the dashboard's API-keys page). Verify against the **raw** body
|
|
107
|
+
before parsing — both SDKs ship a verifier (`Tunova.verify_webhook` / `verifyWebhook`).
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT — see [LICENSE](LICENSE). The SDK code is yours to use freely; your use of the Tunova API is
|
|
112
|
+
governed by the [terms](https://tunova.ai/terms).
|
tunova-0.1.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Tunova SDK
|
|
2
|
+
|
|
3
|
+
Tiny, **zero-dependency** clients + MCP manifest for the [Tunova](https://tunova.ai) music API —
|
|
4
|
+
generate music with Suno (v5.5) over a simple REST or MCP interface.
|
|
5
|
+
Generation is async and **billed only on success**: a failed render refunds itself.
|
|
6
|
+
|
|
7
|
+
- **Python** → [`python/tunova.py`](python/tunova.py) — stdlib only, Python 3.8+.
|
|
8
|
+
- **Node / TypeScript** → [`node/tunova.ts`](node/tunova.ts) — Node 18+ (`fetch` built in).
|
|
9
|
+
- **MCP** → [`server.json`](server.json) — hosted Streamable-HTTP server at `https://api.tunova.ai/mcp`.
|
|
10
|
+
|
|
11
|
+
Get a key (50 free tokens, no card) at **<https://tunova.ai>**. Full reference:
|
|
12
|
+
<https://api.tunova.ai/docs> · live status: <https://tunova.ai/status>.
|
|
13
|
+
|
|
14
|
+
> Tunova is an independent service, not affiliated with or endorsed by Suno. Tracks come from paid
|
|
15
|
+
> Suno plans; review Suno's terms for your use case.
|
|
16
|
+
|
|
17
|
+
## Python
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install tunova
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from tunova import Tunova
|
|
25
|
+
|
|
26
|
+
t = Tunova("sk_live_…")
|
|
27
|
+
job = t.generate("warm lo-fi piano to study to", model="v5.5")
|
|
28
|
+
print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Prefer no install? `python/tunova.py` is stdlib-only — vendor the single file and `import` it.
|
|
32
|
+
|
|
33
|
+
## Node / TypeScript
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm i tunova
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Tunova } from "tunova";
|
|
41
|
+
|
|
42
|
+
const t = new Tunova(process.env.TUNOVA_API_KEY!);
|
|
43
|
+
const job = await t.generate("warm lo-fi piano to study to", { model: "v5.5" });
|
|
44
|
+
console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Both `generate()` calls submit a job and poll until the track is delivered. Prefer fire-and-forget?
|
|
48
|
+
Use `submit()` with a `callback_url` and take an HMAC-signed webhook instead.
|
|
49
|
+
|
|
50
|
+
## MCP — give an AI agent the power to make music
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add --transport http tunova https://api.tunova.ai/mcp \
|
|
54
|
+
--header "X-API-Key: sk_live_…"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Tools: `generate_song` · `wait_for_song` · `check_song`. Same API key, same billed-on-success rule.
|
|
58
|
+
|
|
59
|
+
## Verifying webhooks
|
|
60
|
+
|
|
61
|
+
If you pass `callback_url`, Tunova POSTs the terminal job with
|
|
62
|
+
`X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<rawBody>`, keyed with your
|
|
63
|
+
`whsec_…` secret (view/rotate it on the dashboard's API-keys page). Verify against the **raw** body
|
|
64
|
+
before parsing — both SDKs ship a verifier (`Tunova.verify_webhook` / `verifyWebhook`).
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT — see [LICENSE](LICENSE). The SDK code is yours to use freely; your use of the Tunova API is
|
|
69
|
+
governed by the [terms](https://tunova.ai/terms).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tunova"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tiny zero-dependency Python client for the Tunova Suno music API (v5.5) — async, billed only on success."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Tunova" }]
|
|
13
|
+
keywords = ["suno", "suno-api", "music", "ai", "mcp", "api-client"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://tunova.ai"
|
|
26
|
+
Documentation = "https://api.tunova.ai/docs"
|
|
27
|
+
Source = "https://github.com/erliona/tunova-sdk"
|
|
28
|
+
|
|
29
|
+
# The client is a single stdlib-only module living in python/tunova.py.
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
py-modules = ["tunova"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-dir]
|
|
34
|
+
"" = "python"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunova
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tiny zero-dependency Python client for the Tunova Suno music API (v5.5) — async, billed only on success.
|
|
5
|
+
Author: Tunova
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Tunova
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://tunova.ai
|
|
29
|
+
Project-URL: Documentation, https://api.tunova.ai/docs
|
|
30
|
+
Project-URL: Source, https://github.com/erliona/tunova-sdk
|
|
31
|
+
Keywords: suno,suno-api,music,ai,mcp,api-client
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
38
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
39
|
+
Requires-Python: >=3.8
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Tunova SDK
|
|
45
|
+
|
|
46
|
+
Tiny, **zero-dependency** clients + MCP manifest for the [Tunova](https://tunova.ai) music API —
|
|
47
|
+
generate music with Suno (v5.5) over a simple REST or MCP interface.
|
|
48
|
+
Generation is async and **billed only on success**: a failed render refunds itself.
|
|
49
|
+
|
|
50
|
+
- **Python** → [`python/tunova.py`](python/tunova.py) — stdlib only, Python 3.8+.
|
|
51
|
+
- **Node / TypeScript** → [`node/tunova.ts`](node/tunova.ts) — Node 18+ (`fetch` built in).
|
|
52
|
+
- **MCP** → [`server.json`](server.json) — hosted Streamable-HTTP server at `https://api.tunova.ai/mcp`.
|
|
53
|
+
|
|
54
|
+
Get a key (50 free tokens, no card) at **<https://tunova.ai>**. Full reference:
|
|
55
|
+
<https://api.tunova.ai/docs> · live status: <https://tunova.ai/status>.
|
|
56
|
+
|
|
57
|
+
> Tunova is an independent service, not affiliated with or endorsed by Suno. Tracks come from paid
|
|
58
|
+
> Suno plans; review Suno's terms for your use case.
|
|
59
|
+
|
|
60
|
+
## Python
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install tunova
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from tunova import Tunova
|
|
68
|
+
|
|
69
|
+
t = Tunova("sk_live_…")
|
|
70
|
+
job = t.generate("warm lo-fi piano to study to", model="v5.5")
|
|
71
|
+
print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Prefer no install? `python/tunova.py` is stdlib-only — vendor the single file and `import` it.
|
|
75
|
+
|
|
76
|
+
## Node / TypeScript
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm i tunova
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Tunova } from "tunova";
|
|
84
|
+
|
|
85
|
+
const t = new Tunova(process.env.TUNOVA_API_KEY!);
|
|
86
|
+
const job = await t.generate("warm lo-fi piano to study to", { model: "v5.5" });
|
|
87
|
+
console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Both `generate()` calls submit a job and poll until the track is delivered. Prefer fire-and-forget?
|
|
91
|
+
Use `submit()` with a `callback_url` and take an HMAC-signed webhook instead.
|
|
92
|
+
|
|
93
|
+
## MCP — give an AI agent the power to make music
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
claude mcp add --transport http tunova https://api.tunova.ai/mcp \
|
|
97
|
+
--header "X-API-Key: sk_live_…"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Tools: `generate_song` · `wait_for_song` · `check_song`. Same API key, same billed-on-success rule.
|
|
101
|
+
|
|
102
|
+
## Verifying webhooks
|
|
103
|
+
|
|
104
|
+
If you pass `callback_url`, Tunova POSTs the terminal job with
|
|
105
|
+
`X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<rawBody>`, keyed with your
|
|
106
|
+
`whsec_…` secret (view/rotate it on the dashboard's API-keys page). Verify against the **raw** body
|
|
107
|
+
before parsing — both SDKs ship a verifier (`Tunova.verify_webhook` / `verifyWebhook`).
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT — see [LICENSE](LICENSE). The SDK code is yours to use freely; your use of the Tunova API is
|
|
112
|
+
governed by the [terms](https://tunova.ai/terms).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tunova
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Tunova — tiny zero-dependency Python client for the Suno music API (https://api.tunova.ai).
|
|
2
|
+
|
|
3
|
+
Generation is asynchronous: ``submit()`` returns a ``job_id``; ``generate()`` submits and
|
|
4
|
+
polls until the track is delivered. You're billed only when a song actually delivers —
|
|
5
|
+
failed renders are auto-refunded.
|
|
6
|
+
|
|
7
|
+
from tunova import Tunova
|
|
8
|
+
|
|
9
|
+
t = Tunova("sk_live_…")
|
|
10
|
+
job = t.generate("lofi hip hop to code to", model="v5.5")
|
|
11
|
+
if job["status"] == "complete":
|
|
12
|
+
print(job["clips"][0]["audio_url"])
|
|
13
|
+
else:
|
|
14
|
+
print("failed (auto-refunded):", job["error"])
|
|
15
|
+
|
|
16
|
+
Stdlib only — no pip install needed. Python 3.8+.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import hmac
|
|
22
|
+
import json
|
|
23
|
+
import time
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.request
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE = "https://api.tunova.ai"
|
|
29
|
+
_TERMINAL = ("complete", "failed")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TunovaError(Exception):
|
|
33
|
+
"""An API error. ``code``/``detail`` mirror the JSON error envelope; ``request_id`` is the
|
|
34
|
+
X-Request-Id you can quote to support."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, status: int, code: str, detail: str, request_id: Optional[str] = None):
|
|
37
|
+
super().__init__(f"[{status}] {code}: {detail}")
|
|
38
|
+
self.status = status
|
|
39
|
+
self.code = code
|
|
40
|
+
self.detail = detail
|
|
41
|
+
self.request_id = request_id
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Tunova:
|
|
45
|
+
def __init__(self, api_key: str, base_url: str = DEFAULT_BASE, timeout: float = 30.0):
|
|
46
|
+
if not api_key:
|
|
47
|
+
raise ValueError("api_key is required (your sk_live_… key)")
|
|
48
|
+
self.api_key = api_key
|
|
49
|
+
self.base_url = base_url.rstrip("/")
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
|
|
52
|
+
# ---- transport ----
|
|
53
|
+
def _request(self, method: str, path: str, headers: Dict[str, str], data: Optional[bytes]) -> Any:
|
|
54
|
+
req = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
|
|
55
|
+
try:
|
|
56
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
57
|
+
raw = resp.read()
|
|
58
|
+
return json.loads(raw) if raw else {}
|
|
59
|
+
except urllib.error.HTTPError as e:
|
|
60
|
+
raw = e.read()
|
|
61
|
+
try:
|
|
62
|
+
body = json.loads(raw)
|
|
63
|
+
except Exception:
|
|
64
|
+
body = {}
|
|
65
|
+
raise TunovaError(
|
|
66
|
+
e.code,
|
|
67
|
+
body.get("code", "HTTP_ERROR"),
|
|
68
|
+
body.get("detail", raw.decode("utf-8", "replace")[:300]),
|
|
69
|
+
body.get("request_id"),
|
|
70
|
+
) from None
|
|
71
|
+
|
|
72
|
+
def _post(self, path: str, body: Dict[str, Any], idempotency_key: Optional[str] = None) -> Any:
|
|
73
|
+
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
74
|
+
if idempotency_key:
|
|
75
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
76
|
+
return self._request("POST", path, headers, json.dumps(body).encode())
|
|
77
|
+
|
|
78
|
+
def _get(self, path: str) -> Any:
|
|
79
|
+
return self._request("GET", path, {"X-API-Key": self.api_key}, None)
|
|
80
|
+
|
|
81
|
+
# ---- API ----
|
|
82
|
+
def submit(
|
|
83
|
+
self,
|
|
84
|
+
prompt: str,
|
|
85
|
+
*,
|
|
86
|
+
custom: bool = False,
|
|
87
|
+
tags: Optional[str] = None,
|
|
88
|
+
title: Optional[str] = None,
|
|
89
|
+
make_instrumental: bool = False,
|
|
90
|
+
model: Optional[str] = None,
|
|
91
|
+
callback_url: Optional[str] = None,
|
|
92
|
+
idempotency_key: Optional[str] = None,
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""Submit a generation job (returns immediately). Response: ``{job_id, status, status_url}``.
|
|
95
|
+
|
|
96
|
+
``custom=True`` switches to lyrics mode (``prompt`` = your lyrics; add ``tags``/``title``).
|
|
97
|
+
``model`` is e.g. "v5.5". Pass ``callback_url`` for an HMAC-signed webhook on
|
|
98
|
+
completion, or poll ``get_job``. ``idempotency_key`` makes a retried submit return the same
|
|
99
|
+
job (never double-charged)."""
|
|
100
|
+
body: Dict[str, Any] = {"prompt": prompt}
|
|
101
|
+
if custom:
|
|
102
|
+
if tags is not None:
|
|
103
|
+
body["tags"] = tags
|
|
104
|
+
if title is not None:
|
|
105
|
+
body["title"] = title
|
|
106
|
+
if make_instrumental:
|
|
107
|
+
body["make_instrumental"] = True
|
|
108
|
+
if model:
|
|
109
|
+
body["model"] = model
|
|
110
|
+
if callback_url:
|
|
111
|
+
body["callback_url"] = callback_url
|
|
112
|
+
path = "/api/custom_generate" if custom else "/api/generate"
|
|
113
|
+
return self._post(path, body, idempotency_key)
|
|
114
|
+
|
|
115
|
+
def get_job(self, job_id: str) -> Dict[str, Any]:
|
|
116
|
+
"""Fetch a job's current state. Once ``status == "complete"``, ``clips[].audio_url`` is set."""
|
|
117
|
+
return self._get(f"/api/jobs/{job_id}")
|
|
118
|
+
|
|
119
|
+
def wait_for(self, job_id: str, *, poll_interval: float = 3.0, timeout: float = 300.0) -> Dict[str, Any]:
|
|
120
|
+
"""Poll until the job is terminal ("complete"/"failed") or ``timeout`` seconds elapse."""
|
|
121
|
+
deadline = time.monotonic() + timeout
|
|
122
|
+
while True:
|
|
123
|
+
job = self.get_job(job_id)
|
|
124
|
+
if job.get("status") in _TERMINAL:
|
|
125
|
+
return job
|
|
126
|
+
if time.monotonic() >= deadline:
|
|
127
|
+
raise TunovaError(0, "TIMEOUT", f"job {job_id} not terminal after {timeout}s")
|
|
128
|
+
time.sleep(poll_interval)
|
|
129
|
+
|
|
130
|
+
def generate(self, prompt: str, *, poll_interval: float = 3.0, timeout: float = 300.0, **kwargs: Any) -> Dict[str, Any]:
|
|
131
|
+
"""``submit`` + ``wait_for``. Returns the terminal job — check ``job["status"]``
|
|
132
|
+
("complete" or "failed"). All ``submit`` keyword args are forwarded."""
|
|
133
|
+
accepted = self.submit(prompt, **kwargs)
|
|
134
|
+
return self.wait_for(accepted["job_id"], poll_interval=poll_interval, timeout=timeout)
|
|
135
|
+
|
|
136
|
+
# ---- webhooks ----
|
|
137
|
+
@staticmethod
|
|
138
|
+
def verify_webhook(
|
|
139
|
+
secret: str,
|
|
140
|
+
timestamp: str,
|
|
141
|
+
body: str,
|
|
142
|
+
signature: str,
|
|
143
|
+
*,
|
|
144
|
+
tolerance: int = 300,
|
|
145
|
+
) -> bool:
|
|
146
|
+
"""Verify a webhook delivery. ``signature`` = the ``X-Webhook-Signature`` header
|
|
147
|
+
("sha256=<hex>"), ``timestamp`` = ``X-Webhook-Timestamp``, ``body`` = the RAW request body
|
|
148
|
+
string (verify before parsing), ``secret`` = your ``whsec_…`` key. Returns True iff the
|
|
149
|
+
signature matches AND the timestamp is within ``tolerance`` seconds (anti-replay; pass
|
|
150
|
+
``tolerance=0`` to skip the freshness check)."""
|
|
151
|
+
if tolerance:
|
|
152
|
+
try:
|
|
153
|
+
if abs(time.time() - int(timestamp)) > tolerance:
|
|
154
|
+
return False
|
|
155
|
+
except (TypeError, ValueError):
|
|
156
|
+
return False
|
|
157
|
+
expected = hmac.new(secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest()
|
|
158
|
+
provided = signature[7:] if signature.startswith("sha256=") else signature
|
|
159
|
+
return hmac.compare_digest(expected, provided)
|
tunova-0.1.0/setup.cfg
ADDED