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.
Files changed (82) hide show
  1. ai302/__init__.py +3 -0
  2. ai302/__main__.py +14 -0
  3. ai302/api/__init__.py +5 -0
  4. ai302/api/file.py +231 -0
  5. ai302/api/http_client.py +75 -0
  6. ai302/api/image.py +117 -0
  7. ai302/api/record.py +16 -0
  8. ai302/api/search.py +73 -0
  9. ai302/api/sfx.py +42 -0
  10. ai302/api/song.py +230 -0
  11. ai302/api/stt.py +52 -0
  12. ai302/api/three_d.py +127 -0
  13. ai302/api/tts.py +82 -0
  14. ai302/api/video.py +68 -0
  15. ai302/assets/CLAUDE.md +201 -0
  16. ai302/assets/commands/302ai-media-studio/apply.md +25 -0
  17. ai302/assets/commands/302ai-media-studio/propose.md +25 -0
  18. ai302/assets/commands/302ai-media-studio/result.md +25 -0
  19. ai302/assets/docs/.gitkeep +0 -0
  20. ai302/assets/docs/302ai-cli-agent-guide/cli-agent-guide.md +206 -0
  21. ai302/assets/docs/302ai-cli-agent-guide/commands/file.md +116 -0
  22. ai302/assets/docs/302ai-cli-agent-guide/commands/history.md +82 -0
  23. ai302/assets/docs/302ai-cli-agent-guide/commands/image.md +134 -0
  24. ai302/assets/docs/302ai-cli-agent-guide/commands/model.md +111 -0
  25. ai302/assets/docs/302ai-cli-agent-guide/commands/record.md +72 -0
  26. ai302/assets/docs/302ai-cli-agent-guide/commands/sfx.md +125 -0
  27. ai302/assets/docs/302ai-cli-agent-guide/commands/song.md +272 -0
  28. ai302/assets/docs/302ai-cli-agent-guide/commands/stt.md +76 -0
  29. ai302/assets/docs/302ai-cli-agent-guide/commands/task.md +87 -0
  30. ai302/assets/docs/302ai-cli-agent-guide/commands/tts.md +189 -0
  31. ai302/assets/docs/302ai-cli-agent-guide/commands/video.md +101 -0
  32. ai302/assets/docs/302ai-workflow-guide/workflow-guide.md +716 -0
  33. ai302/assets/docs/cli-guide.md +72 -0
  34. ai302/assets/docs/tasks.yaml +53 -0
  35. ai302/assets/model_params.json +2083 -0
  36. ai302/assets/skills/302ai-media-studio-apply/SKILL.md +221 -0
  37. ai302/assets/skills/302ai-media-studio-apply/references/audio.md +72 -0
  38. ai302/assets/skills/302ai-media-studio-apply/references/image.md +20 -0
  39. ai302/assets/skills/302ai-media-studio-apply/references/video.md +44 -0
  40. ai302/assets/skills/302ai-media-studio-propose/SKILL.md +290 -0
  41. ai302/assets/skills/302ai-media-studio-propose/references/audio.md +645 -0
  42. ai302/assets/skills/302ai-media-studio-propose/references/image.md +391 -0
  43. ai302/assets/skills/302ai-media-studio-propose/references/testing.md +36 -0
  44. ai302/assets/skills/302ai-media-studio-propose/references/video.md +307 -0
  45. ai302/assets/skills/302ai-media-studio-result/SKILL.md +197 -0
  46. ai302/assets/skills/302ai-media-studio-result/references/audio.md +204 -0
  47. ai302/assets/skills/302ai-media-studio-result/references/image.md +88 -0
  48. ai302/assets/skills/302ai-media-studio-result/references/video.md +87 -0
  49. ai302/assets/skills/302ai-search/SKILL.md +170 -0
  50. ai302/assets/skills/302ai-search/scripts/302ai-search.py +407 -0
  51. ai302/assets/stt_models.json +80 -0
  52. ai302/cli/__init__.py +0 -0
  53. ai302/cli/app.py +84 -0
  54. ai302/cli/common.py +50 -0
  55. ai302/cli/file.py +315 -0
  56. ai302/cli/history.py +47 -0
  57. ai302/cli/image.py +499 -0
  58. ai302/cli/init_skills.py +48 -0
  59. ai302/cli/model.py +59 -0
  60. ai302/cli/record.py +49 -0
  61. ai302/cli/search.py +131 -0
  62. ai302/cli/sfx.py +197 -0
  63. ai302/cli/song.py +762 -0
  64. ai302/cli/stt.py +119 -0
  65. ai302/cli/task.py +58 -0
  66. ai302/cli/three_d.py +300 -0
  67. ai302/cli/tts.py +292 -0
  68. ai302/cli/video.py +273 -0
  69. ai302/core/__init__.py +0 -0
  70. ai302/core/config.py +54 -0
  71. ai302/core/errors.py +13 -0
  72. ai302/core/history.py +85 -0
  73. ai302/core/models.py +70 -0
  74. ai302/core/redact.py +21 -0
  75. ai302/core/snapshot.py +71 -0
  76. ai302/core/task_store.py +59 -0
  77. ai302/core/tts_cache.py +86 -0
  78. ai302/core/yaml_edit.py +268 -0
  79. cli_302ai-1.0.2b1.dist-info/METADATA +325 -0
  80. cli_302ai-1.0.2b1.dist-info/RECORD +82 -0
  81. cli_302ai-1.0.2b1.dist-info/WHEEL +4 -0
  82. cli_302ai-1.0.2b1.dist-info/entry_points.txt +2 -0
ai302/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.7"
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
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ["upload_file"]
4
+
5
+ from .file import upload_file
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")
@@ -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