cli-302ai 1.0.2b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai302/__init__.py +3 -0
- ai302/__main__.py +14 -0
- ai302/api/__init__.py +5 -0
- ai302/api/file.py +231 -0
- ai302/api/http_client.py +75 -0
- ai302/api/image.py +117 -0
- ai302/api/record.py +16 -0
- ai302/api/search.py +73 -0
- ai302/api/sfx.py +42 -0
- ai302/api/song.py +230 -0
- ai302/api/stt.py +52 -0
- ai302/api/three_d.py +127 -0
- ai302/api/tts.py +82 -0
- ai302/api/video.py +68 -0
- ai302/assets/CLAUDE.md +201 -0
- ai302/assets/commands/302ai-media-studio/apply.md +25 -0
- ai302/assets/commands/302ai-media-studio/propose.md +25 -0
- ai302/assets/commands/302ai-media-studio/result.md +25 -0
- ai302/assets/docs/.gitkeep +0 -0
- ai302/assets/docs/302ai-cli-agent-guide/cli-agent-guide.md +206 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/file.md +116 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/history.md +82 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/image.md +134 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/model.md +111 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/record.md +72 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/sfx.md +125 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/song.md +272 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/stt.md +76 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/task.md +87 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/tts.md +189 -0
- ai302/assets/docs/302ai-cli-agent-guide/commands/video.md +101 -0
- ai302/assets/docs/302ai-workflow-guide/workflow-guide.md +716 -0
- ai302/assets/docs/cli-guide.md +72 -0
- ai302/assets/docs/tasks.yaml +53 -0
- ai302/assets/model_params.json +2083 -0
- ai302/assets/skills/302ai-media-studio-apply/SKILL.md +221 -0
- ai302/assets/skills/302ai-media-studio-apply/references/audio.md +72 -0
- ai302/assets/skills/302ai-media-studio-apply/references/image.md +20 -0
- ai302/assets/skills/302ai-media-studio-apply/references/video.md +44 -0
- ai302/assets/skills/302ai-media-studio-propose/SKILL.md +290 -0
- ai302/assets/skills/302ai-media-studio-propose/references/audio.md +645 -0
- ai302/assets/skills/302ai-media-studio-propose/references/image.md +391 -0
- ai302/assets/skills/302ai-media-studio-propose/references/testing.md +36 -0
- ai302/assets/skills/302ai-media-studio-propose/references/video.md +307 -0
- ai302/assets/skills/302ai-media-studio-result/SKILL.md +197 -0
- ai302/assets/skills/302ai-media-studio-result/references/audio.md +204 -0
- ai302/assets/skills/302ai-media-studio-result/references/image.md +88 -0
- ai302/assets/skills/302ai-media-studio-result/references/video.md +87 -0
- ai302/assets/skills/302ai-search/SKILL.md +170 -0
- ai302/assets/skills/302ai-search/scripts/302ai-search.py +407 -0
- ai302/assets/stt_models.json +80 -0
- ai302/cli/__init__.py +0 -0
- ai302/cli/app.py +84 -0
- ai302/cli/common.py +50 -0
- ai302/cli/file.py +315 -0
- ai302/cli/history.py +47 -0
- ai302/cli/image.py +499 -0
- ai302/cli/init_skills.py +48 -0
- ai302/cli/model.py +59 -0
- ai302/cli/record.py +49 -0
- ai302/cli/search.py +131 -0
- ai302/cli/sfx.py +197 -0
- ai302/cli/song.py +762 -0
- ai302/cli/stt.py +119 -0
- ai302/cli/task.py +58 -0
- ai302/cli/three_d.py +300 -0
- ai302/cli/tts.py +292 -0
- ai302/cli/video.py +273 -0
- ai302/core/__init__.py +0 -0
- ai302/core/config.py +54 -0
- ai302/core/errors.py +13 -0
- ai302/core/history.py +85 -0
- ai302/core/models.py +70 -0
- ai302/core/redact.py +21 -0
- ai302/core/snapshot.py +71 -0
- ai302/core/task_store.py +59 -0
- ai302/core/tts_cache.py +86 -0
- ai302/core/yaml_edit.py +268 -0
- cli_302ai-1.0.2b1.dist-info/METADATA +325 -0
- cli_302ai-1.0.2b1.dist-info/RECORD +82 -0
- cli_302ai-1.0.2b1.dist-info/WHEEL +4 -0
- cli_302ai-1.0.2b1.dist-info/entry_points.txt +2 -0
ai302/__init__.py
ADDED
ai302/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
|
5
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
6
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
7
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
8
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
9
|
+
|
|
10
|
+
from .cli.app import app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
app(prog_name="302ai")
|
ai302/api/__init__.py
ADDED
ai302/api/file.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .http_client import AI302ApiError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def upload_file(
|
|
14
|
+
*,
|
|
15
|
+
url: str,
|
|
16
|
+
file_path: str,
|
|
17
|
+
prefix: str = "img",
|
|
18
|
+
timeout_s: float = 60.0,
|
|
19
|
+
retries: int = 2,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
path = Path(file_path)
|
|
22
|
+
content_type, _enc = mimetypes.guess_type(str(path))
|
|
23
|
+
|
|
24
|
+
return _upload_bytes(
|
|
25
|
+
url=url,
|
|
26
|
+
filename=path.name,
|
|
27
|
+
content=path.read_bytes(),
|
|
28
|
+
content_type=content_type or "application/octet-stream",
|
|
29
|
+
prefix=prefix,
|
|
30
|
+
timeout_s=timeout_s,
|
|
31
|
+
retries=retries,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def delete_uploaded_file(
|
|
36
|
+
*,
|
|
37
|
+
url: str,
|
|
38
|
+
file_path: str,
|
|
39
|
+
timeout_s: float = 60.0,
|
|
40
|
+
retries: int = 2,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
for attempt in range(retries + 1):
|
|
43
|
+
try:
|
|
44
|
+
with httpx.Client(timeout=timeout_s) as client:
|
|
45
|
+
resp = client.request("DELETE", url, json={"file_path": file_path})
|
|
46
|
+
if 200 <= resp.status_code < 300:
|
|
47
|
+
payload = resp.json()
|
|
48
|
+
if not isinstance(payload, dict):
|
|
49
|
+
raise AI302ApiError("delete failed: invalid response", status_code=resp.status_code)
|
|
50
|
+
return payload
|
|
51
|
+
|
|
52
|
+
payload: Any = None
|
|
53
|
+
try:
|
|
54
|
+
payload = resp.json()
|
|
55
|
+
except Exception:
|
|
56
|
+
payload = None
|
|
57
|
+
|
|
58
|
+
msg = "unknown error"
|
|
59
|
+
if isinstance(payload, dict) and isinstance(payload.get("msg"), str) and payload.get("msg").strip():
|
|
60
|
+
msg = payload.get("msg").strip()
|
|
61
|
+
|
|
62
|
+
retryable = resp.status_code >= 500
|
|
63
|
+
if attempt < retries and retryable:
|
|
64
|
+
continue
|
|
65
|
+
raise AI302ApiError(msg, status_code=resp.status_code, retryable=retryable)
|
|
66
|
+
|
|
67
|
+
except Exception as e: # noqa: BLE001
|
|
68
|
+
if attempt < retries and isinstance(e, (httpx.TimeoutException, httpx.NetworkError)):
|
|
69
|
+
continue
|
|
70
|
+
if isinstance(e, AI302ApiError):
|
|
71
|
+
raise
|
|
72
|
+
if isinstance(e, httpx.TimeoutException):
|
|
73
|
+
raise AI302ApiError("request timeout", retryable=True) from e
|
|
74
|
+
if isinstance(e, httpx.NetworkError):
|
|
75
|
+
raise AI302ApiError("network error", retryable=True) from e
|
|
76
|
+
raise AI302ApiError("unknown error") from e
|
|
77
|
+
|
|
78
|
+
raise AI302ApiError("unknown error")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def upload_file_from_url(
|
|
82
|
+
*,
|
|
83
|
+
url: str,
|
|
84
|
+
file_url: str,
|
|
85
|
+
prefix: str = "img",
|
|
86
|
+
timeout_s: float = 60.0,
|
|
87
|
+
retries: int = 2,
|
|
88
|
+
debug: bool = False,
|
|
89
|
+
debug_download_to: str | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
import sys
|
|
92
|
+
for attempt in range(retries + 1):
|
|
93
|
+
try:
|
|
94
|
+
if debug:
|
|
95
|
+
print(f"[ai302:file:upload-url] attempt={attempt} downloading {file_url}", file=sys.stderr)
|
|
96
|
+
with httpx.Client(timeout=timeout_s, follow_redirects=True) as client:
|
|
97
|
+
resp = client.get(file_url, headers={"Accept-Encoding": "identity"})
|
|
98
|
+
if debug:
|
|
99
|
+
size = int(resp.headers.get("content-length") or 0)
|
|
100
|
+
print(
|
|
101
|
+
f"[ai302:file:upload-url] download status={resp.status_code} content_type={resp.headers.get('content-type')} content_length={size}",
|
|
102
|
+
file=__import__("sys").stderr,
|
|
103
|
+
)
|
|
104
|
+
if 200 <= resp.status_code < 300:
|
|
105
|
+
content_type = resp.headers.get("content-type")
|
|
106
|
+
orig = Path(httpx.URL(file_url).path).name
|
|
107
|
+
suffix = Path(orig).suffix
|
|
108
|
+
filename = f"{__import__('uuid').uuid4().hex}{suffix}" if suffix else __import__('uuid').uuid4().hex
|
|
109
|
+
if debug:
|
|
110
|
+
print(
|
|
111
|
+
f"[ai302:file:upload-url] uploading filename={filename} bytes={len(resp.content)}",
|
|
112
|
+
file=__import__("sys").stderr,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if debug_download_to is not None:
|
|
116
|
+
Path(debug_download_to).write_bytes(resp.content)
|
|
117
|
+
if debug:
|
|
118
|
+
print(
|
|
119
|
+
f"[ai302:file:upload-url] saved download to {debug_download_to}",
|
|
120
|
+
file=__import__("sys").stderr,
|
|
121
|
+
)
|
|
122
|
+
return _upload_bytes(
|
|
123
|
+
url=url,
|
|
124
|
+
filename=filename,
|
|
125
|
+
content=resp.content,
|
|
126
|
+
content_type=content_type or "application/octet-stream",
|
|
127
|
+
prefix=prefix,
|
|
128
|
+
timeout_s=timeout_s,
|
|
129
|
+
retries=retries,
|
|
130
|
+
debug=debug,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
payload: Any = None
|
|
134
|
+
try:
|
|
135
|
+
payload = resp.json()
|
|
136
|
+
except Exception:
|
|
137
|
+
payload = None
|
|
138
|
+
|
|
139
|
+
msg = "download failed"
|
|
140
|
+
if isinstance(payload, dict) and isinstance(payload.get("msg"), str) and payload.get("msg").strip():
|
|
141
|
+
msg = payload.get("msg").strip()
|
|
142
|
+
|
|
143
|
+
retryable = resp.status_code >= 500
|
|
144
|
+
if attempt < retries and retryable:
|
|
145
|
+
continue
|
|
146
|
+
raise AI302ApiError(msg, status_code=resp.status_code, retryable=retryable)
|
|
147
|
+
|
|
148
|
+
except Exception as e: # noqa: BLE001
|
|
149
|
+
if debug:
|
|
150
|
+
print(f"[ai302:file:upload-url] exception: {type(e).__name__}: {e}", file=sys.stderr)
|
|
151
|
+
if attempt < retries and isinstance(e, (httpx.TimeoutException, httpx.NetworkError)):
|
|
152
|
+
continue
|
|
153
|
+
if isinstance(e, AI302ApiError):
|
|
154
|
+
raise
|
|
155
|
+
if isinstance(e, httpx.TimeoutException):
|
|
156
|
+
raise AI302ApiError("request timeout", retryable=True) from e
|
|
157
|
+
if isinstance(e, httpx.NetworkError):
|
|
158
|
+
raise AI302ApiError("network error", retryable=True) from e
|
|
159
|
+
raise AI302ApiError(f"unknown error: {type(e).__name__}: {e}") from e
|
|
160
|
+
|
|
161
|
+
raise AI302ApiError("unknown error")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _upload_bytes(
|
|
165
|
+
*,
|
|
166
|
+
url: str,
|
|
167
|
+
filename: str,
|
|
168
|
+
content: bytes,
|
|
169
|
+
content_type: str,
|
|
170
|
+
prefix: str,
|
|
171
|
+
timeout_s: float,
|
|
172
|
+
retries: int,
|
|
173
|
+
debug: bool = False,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
|
|
176
|
+
data: dict[str, str] = {"prefix": prefix, "file_name": filename, "url": "", "base64_data": ""}
|
|
177
|
+
|
|
178
|
+
for attempt in range(retries + 1):
|
|
179
|
+
try:
|
|
180
|
+
with httpx.Client(timeout=timeout_s) as client:
|
|
181
|
+
files = {"file": (filename, content, content_type)}
|
|
182
|
+
|
|
183
|
+
resp = client.post(url, files=files, data=data, timeout=60)
|
|
184
|
+
if debug:
|
|
185
|
+
print(
|
|
186
|
+
f"[ai302:file:upload-url] upload status={resp.status_code}",
|
|
187
|
+
file=__import__("sys").stderr,
|
|
188
|
+
)
|
|
189
|
+
if 200 <= resp.status_code < 300:
|
|
190
|
+
payload = resp.json()
|
|
191
|
+
if not isinstance(payload, dict):
|
|
192
|
+
raise AI302ApiError("upload failed: invalid response", status_code=resp.status_code)
|
|
193
|
+
|
|
194
|
+
if payload.get("code") != 0:
|
|
195
|
+
msg = payload.get("msg") if isinstance(payload.get("msg"), str) else "upload failed"
|
|
196
|
+
raise AI302ApiError(msg, status_code=resp.status_code, retryable=False)
|
|
197
|
+
|
|
198
|
+
data = payload.get("data")
|
|
199
|
+
uploaded_url = data.get("url") if isinstance(data, dict) else None
|
|
200
|
+
if not isinstance(uploaded_url, str) or not uploaded_url.strip():
|
|
201
|
+
raise AI302ApiError("upload failed: missing url", status_code=resp.status_code)
|
|
202
|
+
|
|
203
|
+
return payload
|
|
204
|
+
|
|
205
|
+
payload: Any = None
|
|
206
|
+
try:
|
|
207
|
+
payload = resp.json()
|
|
208
|
+
except Exception:
|
|
209
|
+
payload = None
|
|
210
|
+
|
|
211
|
+
msg = "unknown error"
|
|
212
|
+
if isinstance(payload, dict) and isinstance(payload.get("msg"), str) and payload.get("msg").strip():
|
|
213
|
+
msg = payload.get("msg").strip()
|
|
214
|
+
|
|
215
|
+
retryable = resp.status_code >= 500
|
|
216
|
+
if attempt < retries and retryable:
|
|
217
|
+
continue
|
|
218
|
+
raise AI302ApiError(msg, status_code=resp.status_code, retryable=retryable)
|
|
219
|
+
|
|
220
|
+
except Exception as e: # noqa: BLE001
|
|
221
|
+
if attempt < retries and isinstance(e, (httpx.TimeoutException, httpx.NetworkError)):
|
|
222
|
+
continue
|
|
223
|
+
if isinstance(e, AI302ApiError):
|
|
224
|
+
raise
|
|
225
|
+
if isinstance(e, httpx.TimeoutException):
|
|
226
|
+
raise AI302ApiError("request timeout", retryable=True) from e
|
|
227
|
+
if isinstance(e, httpx.NetworkError):
|
|
228
|
+
raise AI302ApiError("network error", retryable=True) from e
|
|
229
|
+
raise AI302ApiError("unknown error") from e
|
|
230
|
+
|
|
231
|
+
raise AI302ApiError("unknown error")
|
ai302/api/http_client.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from ..core.errors import extract_error_message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = os.environ.get("AI302_BASE_URL", "https://api.302ai.com")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AI302ApiError(RuntimeError):
|
|
15
|
+
def __init__(self, message: str, *, status_code: int | None = None, retryable: bool = False):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.retryable = retryable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _auth_header(api_key: str) -> dict[str, str]:
|
|
22
|
+
return {"Authorization": f"Bearer {api_key}"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _should_retry(resp: httpx.Response | None, exc: Exception | None) -> bool:
|
|
26
|
+
if resp is not None:
|
|
27
|
+
if resp.status_code == 401:
|
|
28
|
+
return False
|
|
29
|
+
return resp.status_code >= 500 or resp.status_code in (408, 409, 429)
|
|
30
|
+
return isinstance(exc, (httpx.TimeoutException, httpx.NetworkError))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def request_json(
|
|
34
|
+
*,
|
|
35
|
+
method: str,
|
|
36
|
+
url: str,
|
|
37
|
+
api_key: str,
|
|
38
|
+
params: dict[str, Any] | list[tuple[str, str]] | None = None,
|
|
39
|
+
json_body: dict[str, Any] | None = None,
|
|
40
|
+
timeout_s: float = 60.0,
|
|
41
|
+
retries: int = 2,
|
|
42
|
+
) -> tuple[dict[str, Any], httpx.Headers]:
|
|
43
|
+
with httpx.Client(timeout=timeout_s, headers=_auth_header(api_key)) as client:
|
|
44
|
+
for attempt in range(retries + 1):
|
|
45
|
+
try:
|
|
46
|
+
resp = client.request(method, url, params=params, json=json_body)
|
|
47
|
+
if 200 <= resp.status_code < 300:
|
|
48
|
+
data = resp.json()
|
|
49
|
+
if isinstance(data, dict):
|
|
50
|
+
return data, resp.headers
|
|
51
|
+
raise AI302ApiError("unknown error", status_code=resp.status_code)
|
|
52
|
+
|
|
53
|
+
payload: Any = None
|
|
54
|
+
try:
|
|
55
|
+
payload = resp.json()
|
|
56
|
+
except Exception:
|
|
57
|
+
payload = None
|
|
58
|
+
|
|
59
|
+
msg = extract_error_message(payload)
|
|
60
|
+
if attempt < retries and _should_retry(resp, None):
|
|
61
|
+
continue
|
|
62
|
+
raise AI302ApiError(msg, status_code=resp.status_code, retryable=_should_retry(resp, None))
|
|
63
|
+
|
|
64
|
+
except Exception as e: # noqa: BLE001
|
|
65
|
+
if attempt < retries and _should_retry(None, e):
|
|
66
|
+
continue
|
|
67
|
+
if isinstance(e, AI302ApiError):
|
|
68
|
+
raise
|
|
69
|
+
if isinstance(e, httpx.TimeoutException):
|
|
70
|
+
raise AI302ApiError("request timeout", retryable=True) from e
|
|
71
|
+
if isinstance(e, httpx.NetworkError):
|
|
72
|
+
raise AI302ApiError("network error", retryable=True) from e
|
|
73
|
+
raise AI302ApiError("unknown error") from e
|
|
74
|
+
|
|
75
|
+
raise AI302ApiError("unknown error")
|
ai302/api/image.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .http_client import DEFAULT_BASE_URL, request_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_image_sync(
|
|
9
|
+
*,
|
|
10
|
+
api_key: str,
|
|
11
|
+
model: str,
|
|
12
|
+
prompt: str,
|
|
13
|
+
height: int | None = None,
|
|
14
|
+
width: int | None = None,
|
|
15
|
+
negative_prompt: str | None = None,
|
|
16
|
+
aspect_ratio: str | None = None,
|
|
17
|
+
output_format: str | None = None,
|
|
18
|
+
image: str | list[str] | None = None,
|
|
19
|
+
mask_image: str | None = None,
|
|
20
|
+
extra: dict[str, Any] | None = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
body: dict[str, Any] = {"model": model, "prompt": prompt}
|
|
23
|
+
if height is not None:
|
|
24
|
+
body["height"] = height
|
|
25
|
+
if width is not None:
|
|
26
|
+
body["width"] = width
|
|
27
|
+
if negative_prompt is not None:
|
|
28
|
+
body["negative_prompt"] = negative_prompt
|
|
29
|
+
if aspect_ratio is not None:
|
|
30
|
+
body["aspect_ratio"] = aspect_ratio
|
|
31
|
+
if output_format is not None:
|
|
32
|
+
body["output_format"] = output_format
|
|
33
|
+
if image is not None:
|
|
34
|
+
body["image"] = image
|
|
35
|
+
if mask_image is not None:
|
|
36
|
+
body["mask_image"] = mask_image
|
|
37
|
+
if extra:
|
|
38
|
+
body.update(extra)
|
|
39
|
+
|
|
40
|
+
data, headers = request_json(
|
|
41
|
+
method="POST",
|
|
42
|
+
url=f"{DEFAULT_BASE_URL}/302/v2/image/generate",
|
|
43
|
+
api_key=api_key,
|
|
44
|
+
json_body=body,
|
|
45
|
+
timeout_s=180.0,
|
|
46
|
+
retries=1,
|
|
47
|
+
)
|
|
48
|
+
request_id = headers.get("request-id")
|
|
49
|
+
if request_id:
|
|
50
|
+
data["request_id"] = request_id
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_image_task(
|
|
55
|
+
*,
|
|
56
|
+
api_key: str,
|
|
57
|
+
model: str,
|
|
58
|
+
prompt: str,
|
|
59
|
+
height: int | None = None,
|
|
60
|
+
width: int | None = None,
|
|
61
|
+
negative_prompt: str | None = None,
|
|
62
|
+
aspect_ratio: str | None = None,
|
|
63
|
+
output_format: str | None = None,
|
|
64
|
+
image: str | list[str] | None = None,
|
|
65
|
+
mask_image: str | None = None,
|
|
66
|
+
run_async: bool | None = None,
|
|
67
|
+
webhook: str | None = None,
|
|
68
|
+
extra: dict[str, Any] | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
body: dict[str, Any] = {"model": model, "prompt": prompt}
|
|
71
|
+
if height is not None:
|
|
72
|
+
body["height"] = height
|
|
73
|
+
if width is not None:
|
|
74
|
+
body["width"] = width
|
|
75
|
+
if negative_prompt is not None:
|
|
76
|
+
body["negative_prompt"] = negative_prompt
|
|
77
|
+
if aspect_ratio is not None:
|
|
78
|
+
body["aspect_ratio"] = aspect_ratio
|
|
79
|
+
if output_format is not None:
|
|
80
|
+
body["output_format"] = output_format
|
|
81
|
+
if image is not None:
|
|
82
|
+
body["image"] = image
|
|
83
|
+
if mask_image is not None:
|
|
84
|
+
body["mask_image"] = mask_image
|
|
85
|
+
if extra:
|
|
86
|
+
body.update(extra)
|
|
87
|
+
|
|
88
|
+
params: dict[str, Any] = {}
|
|
89
|
+
if run_async is not None:
|
|
90
|
+
params["run_async"] = run_async
|
|
91
|
+
if webhook:
|
|
92
|
+
params["webhook"] = webhook
|
|
93
|
+
|
|
94
|
+
data, headers = request_json(
|
|
95
|
+
method="POST",
|
|
96
|
+
url=f"{DEFAULT_BASE_URL}/302/v2/image/generate",
|
|
97
|
+
api_key=api_key,
|
|
98
|
+
params=params or None,
|
|
99
|
+
json_body=body,
|
|
100
|
+
timeout_s=180.0,
|
|
101
|
+
retries=1,
|
|
102
|
+
)
|
|
103
|
+
request_id = headers.get("request-id")
|
|
104
|
+
if request_id:
|
|
105
|
+
data["request_id"] = request_id
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def fetch_image_task(*, api_key: str, task_id: str) -> dict[str, Any]:
|
|
110
|
+
data, _headers = request_json(
|
|
111
|
+
method="GET",
|
|
112
|
+
url=f"{DEFAULT_BASE_URL}/302/v2/image/fetch/{task_id}",
|
|
113
|
+
api_key=api_key,
|
|
114
|
+
timeout_s=60.0,
|
|
115
|
+
retries=2,
|
|
116
|
+
)
|
|
117
|
+
return data
|
ai302/api/record.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .http_client import DEFAULT_BASE_URL, request_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_record(*, api_key: str, request_id: str) -> dict[str, Any]:
|
|
9
|
+
data, _headers = request_json(
|
|
10
|
+
method="GET",
|
|
11
|
+
url=f"{DEFAULT_BASE_URL}/dashboard/record/{request_id}",
|
|
12
|
+
api_key=api_key,
|
|
13
|
+
timeout_s=60.0,
|
|
14
|
+
retries=2,
|
|
15
|
+
)
|
|
16
|
+
return data
|
ai302/api/search.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .http_client import DEFAULT_BASE_URL, request_json
|
|
6
|
+
|
|
7
|
+
API_PATH = "/302/general/search"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def search(
|
|
11
|
+
*,
|
|
12
|
+
api_key: str,
|
|
13
|
+
query: str,
|
|
14
|
+
provider: str,
|
|
15
|
+
max_results: int = 5,
|
|
16
|
+
category: str | None = None,
|
|
17
|
+
time_range: str | None = None,
|
|
18
|
+
include_images: bool = True,
|
|
19
|
+
include_domains: list[str] | None = None,
|
|
20
|
+
exclude_domains: list[str] | None = None,
|
|
21
|
+
start_crawl_date: str | None = None,
|
|
22
|
+
end_crawl_date: str | None = None,
|
|
23
|
+
start_published_date: str | None = None,
|
|
24
|
+
end_published_date: str | None = None,
|
|
25
|
+
crawl_results: int | None = None,
|
|
26
|
+
include_row_content: bool | None = None,
|
|
27
|
+
page: int | None = None,
|
|
28
|
+
max_tokens_per_page: int | None = None,
|
|
29
|
+
country: str | None = None,
|
|
30
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
31
|
+
body: dict[str, Any] = {
|
|
32
|
+
"query": query,
|
|
33
|
+
"provider": provider,
|
|
34
|
+
"max_results": max_results,
|
|
35
|
+
"include_images": include_images,
|
|
36
|
+
}
|
|
37
|
+
if category is not None:
|
|
38
|
+
body["category"] = category
|
|
39
|
+
if time_range is not None:
|
|
40
|
+
body["time_range"] = time_range
|
|
41
|
+
if include_domains is not None:
|
|
42
|
+
body["include_domains"] = include_domains
|
|
43
|
+
if exclude_domains is not None:
|
|
44
|
+
body["exclude_domains"] = exclude_domains
|
|
45
|
+
if start_crawl_date is not None:
|
|
46
|
+
body["startCrawlDate"] = start_crawl_date
|
|
47
|
+
if end_crawl_date is not None:
|
|
48
|
+
body["endCrawlDate"] = end_crawl_date
|
|
49
|
+
if start_published_date is not None:
|
|
50
|
+
body["startPublishedDate"] = start_published_date
|
|
51
|
+
if end_published_date is not None:
|
|
52
|
+
body["endPublishedDate"] = end_published_date
|
|
53
|
+
if crawl_results is not None:
|
|
54
|
+
body["crawl_results"] = crawl_results
|
|
55
|
+
if include_row_content is not None:
|
|
56
|
+
body["includeRowContent"] = include_row_content
|
|
57
|
+
if page is not None:
|
|
58
|
+
body["page"] = page
|
|
59
|
+
if max_tokens_per_page is not None:
|
|
60
|
+
body["max_tokens_per_page"] = max_tokens_per_page
|
|
61
|
+
if country is not None:
|
|
62
|
+
body["country"] = country
|
|
63
|
+
|
|
64
|
+
data, headers = request_json(
|
|
65
|
+
method="POST",
|
|
66
|
+
url=f"{DEFAULT_BASE_URL}{API_PATH}",
|
|
67
|
+
api_key=api_key,
|
|
68
|
+
json_body=body,
|
|
69
|
+
timeout_s=30.0,
|
|
70
|
+
retries=2,
|
|
71
|
+
)
|
|
72
|
+
request_id = headers.get("request-id")
|
|
73
|
+
return data, request_id
|
ai302/api/sfx.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .http_client import DEFAULT_BASE_URL, request_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_sfx_task(
|
|
9
|
+
*,
|
|
10
|
+
api_key: str,
|
|
11
|
+
prompt: str,
|
|
12
|
+
duration: float,
|
|
13
|
+
external_task_id: str | None = None,
|
|
14
|
+
callback: str | None = None,
|
|
15
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
16
|
+
body: dict[str, Any] = {"prompt": prompt, "duration": duration, "type": "text-to-audio"}
|
|
17
|
+
if external_task_id is not None:
|
|
18
|
+
body["external_task_id"] = external_task_id
|
|
19
|
+
if callback is not None:
|
|
20
|
+
body["callback"] = callback
|
|
21
|
+
|
|
22
|
+
data, headers = request_json(
|
|
23
|
+
method="POST",
|
|
24
|
+
url=f"{DEFAULT_BASE_URL}/klingai/v1/audio/text-to-audio",
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
json_body=body,
|
|
27
|
+
timeout_s=60.0,
|
|
28
|
+
retries=2,
|
|
29
|
+
)
|
|
30
|
+
request_id = headers.get("request-id")
|
|
31
|
+
return data, request_id
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fetch_sfx_task(*, api_key: str, task_id: str) -> dict[str, Any]:
|
|
35
|
+
data, _headers = request_json(
|
|
36
|
+
method="GET",
|
|
37
|
+
url=f"{DEFAULT_BASE_URL}/klingai/v1/audio/video-to-audio/{task_id}",
|
|
38
|
+
api_key=api_key,
|
|
39
|
+
timeout_s=60.0,
|
|
40
|
+
retries=2,
|
|
41
|
+
)
|
|
42
|
+
return data
|