orbitalsai 1.0.0__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.
- orbitalsai/__init__.py +65 -0
- orbitalsai/async_client.py +379 -0
- orbitalsai/client.py +368 -0
- orbitalsai/exceptions.py +58 -0
- orbitalsai/models.py +135 -0
- orbitalsai/utils.py +98 -0
- orbitalsai-1.0.0.dist-info/METADATA +439 -0
- orbitalsai-1.0.0.dist-info/RECORD +11 -0
- orbitalsai-1.0.0.dist-info/WHEEL +5 -0
- orbitalsai-1.0.0.dist-info/licenses/LICENSE +21 -0
- orbitalsai-1.0.0.dist-info/top_level.txt +1 -0
orbitalsai/client.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI Synchronous Client
|
|
3
|
+
|
|
4
|
+
Synchronous client for the OrbitalsAI API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
from datetime import datetime, date
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .models import (
|
|
14
|
+
TranscriptTask, Transcript, Balance, UsageHistory, DailyUsage,
|
|
15
|
+
User, APIKey, UsageRecord, DailyUsageRecord
|
|
16
|
+
)
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
OrbitalsAIError, AuthenticationError, InsufficientBalanceError,
|
|
19
|
+
FileNotFoundError, UnsupportedFileError, UnsupportedLanguageError,
|
|
20
|
+
TaskNotFoundError, TranscriptionError, TimeoutError, APIError
|
|
21
|
+
)
|
|
22
|
+
from .utils import validate_audio_file, validate_language, get_file_size
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Client:
|
|
26
|
+
"""
|
|
27
|
+
Synchronous client for the OrbitalsAI API.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
client = orbitalsai.Client(api_key="your_api_key_here")
|
|
31
|
+
transcript = client.transcribe("audio.mp3")
|
|
32
|
+
print(transcript.text)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, api_key: str, base_url: str = "https://api.orbitalsai.com/api/v1"):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the OrbitalsAI client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
api_key: Your OrbitalsAI API key
|
|
41
|
+
base_url: Base URL for the API (default: localhost for development)
|
|
42
|
+
"""
|
|
43
|
+
self.api_key = api_key
|
|
44
|
+
self.base_url = base_url.rstrip('/')
|
|
45
|
+
self.session = requests.Session()
|
|
46
|
+
self.session.headers.update({
|
|
47
|
+
"Authorization": f"Bearer {api_key}",
|
|
48
|
+
"User-Agent": "orbitalsai-python-sdk/1.0.0"
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
def _make_request(self, method: str, endpoint: str, **kwargs) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
Make an HTTP request to the API.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
method: HTTP method (GET, POST, etc.)
|
|
57
|
+
endpoint: API endpoint (without base URL)
|
|
58
|
+
**kwargs: Additional arguments for requests
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
JSON response data
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
APIError: If the API returns an error
|
|
65
|
+
"""
|
|
66
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
response = self.session.request(method, url, **kwargs)
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
return response.json()
|
|
72
|
+
except requests.exceptions.HTTPError as e:
|
|
73
|
+
if response.status_code == 401:
|
|
74
|
+
raise AuthenticationError("Invalid API key")
|
|
75
|
+
elif response.status_code == 402:
|
|
76
|
+
raise InsufficientBalanceError("Insufficient balance")
|
|
77
|
+
elif response.status_code == 404:
|
|
78
|
+
raise TaskNotFoundError("Task not found")
|
|
79
|
+
else:
|
|
80
|
+
try:
|
|
81
|
+
error_data = response.json()
|
|
82
|
+
raise APIError(
|
|
83
|
+
error_data.get("detail", str(e)),
|
|
84
|
+
status_code=response.status_code,
|
|
85
|
+
response_data=error_data
|
|
86
|
+
)
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise APIError(str(e), status_code=response.status_code)
|
|
89
|
+
except requests.exceptions.RequestException as e:
|
|
90
|
+
raise OrbitalsAIError(f"Request failed: {str(e)}")
|
|
91
|
+
|
|
92
|
+
def transcribe(
|
|
93
|
+
self,
|
|
94
|
+
file_path: str,
|
|
95
|
+
language: str = "english",
|
|
96
|
+
generate_srt: bool = False,
|
|
97
|
+
wait: bool = True,
|
|
98
|
+
timeout: int = 300,
|
|
99
|
+
poll_interval: int = 5
|
|
100
|
+
) -> Transcript:
|
|
101
|
+
"""
|
|
102
|
+
Transcribe an audio file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
file_path: Path to the audio file
|
|
106
|
+
language: Language of the audio (default: "english")
|
|
107
|
+
generate_srt: Whether to generate SRT subtitles (default: False)
|
|
108
|
+
wait: Whether to wait for completion (default: True)
|
|
109
|
+
timeout: Maximum time to wait in seconds (default: 300)
|
|
110
|
+
poll_interval: Seconds to wait between status checks (default: 5)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Transcript object with the result
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
FileNotFoundError: If the audio file is not found
|
|
117
|
+
UnsupportedFileError: If the file format is not supported
|
|
118
|
+
UnsupportedLanguageError: If the language is not supported
|
|
119
|
+
TimeoutError: If the operation times out
|
|
120
|
+
TranscriptionError: If transcription fails
|
|
121
|
+
"""
|
|
122
|
+
# Validate inputs
|
|
123
|
+
validate_audio_file(file_path)
|
|
124
|
+
validate_language(language)
|
|
125
|
+
|
|
126
|
+
# Prepare file upload
|
|
127
|
+
with open(file_path, 'rb') as f:
|
|
128
|
+
files = {"file": (Path(file_path).name, f, "audio/mpeg")}
|
|
129
|
+
data = {
|
|
130
|
+
"language": language,
|
|
131
|
+
"generate_srt": str(generate_srt).lower()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Upload file
|
|
135
|
+
response = self._make_request("POST", "/audio/upload", files=files, data=data)
|
|
136
|
+
task_id = response["task_id"]
|
|
137
|
+
|
|
138
|
+
if not wait:
|
|
139
|
+
# Return task immediately
|
|
140
|
+
task = self.get_task(task_id)
|
|
141
|
+
return Transcript(
|
|
142
|
+
text=task.result_text or "",
|
|
143
|
+
srt_content=task.srt_content,
|
|
144
|
+
task_id=task_id,
|
|
145
|
+
original_filename=task.original_filename,
|
|
146
|
+
audio_url=task.audio_url
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Wait for completion
|
|
150
|
+
return self.wait_for_task(task_id, timeout, poll_interval)
|
|
151
|
+
|
|
152
|
+
def get_task(self, task_id: int) -> TranscriptTask:
|
|
153
|
+
"""
|
|
154
|
+
Get the status of a transcription task.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
task_id: ID of the task
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
TranscriptTask object with current status
|
|
161
|
+
"""
|
|
162
|
+
response = self._make_request("GET", f"/audio/status/{task_id}")
|
|
163
|
+
|
|
164
|
+
return TranscriptTask(
|
|
165
|
+
task_id=task_id,
|
|
166
|
+
status=response["status"],
|
|
167
|
+
original_filename=response["original_filename"],
|
|
168
|
+
audio_url=response.get("audio_url"),
|
|
169
|
+
srt_requested=response["srt_requested"],
|
|
170
|
+
result_text=response.get("result_text"),
|
|
171
|
+
srt_content=response.get("srt_content"),
|
|
172
|
+
error=response.get("error")
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def wait_for_task(
|
|
176
|
+
self,
|
|
177
|
+
task_id: int,
|
|
178
|
+
timeout: int = 300,
|
|
179
|
+
poll_interval: int = 5
|
|
180
|
+
) -> Transcript:
|
|
181
|
+
"""
|
|
182
|
+
Wait for a transcription task to complete.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
task_id: ID of the task
|
|
186
|
+
timeout: Maximum time to wait in seconds
|
|
187
|
+
poll_interval: Seconds to wait between status checks
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Transcript object with the result
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
TimeoutError: If the operation times out
|
|
194
|
+
TranscriptionError: If transcription fails
|
|
195
|
+
"""
|
|
196
|
+
start_time = time.time()
|
|
197
|
+
|
|
198
|
+
while time.time() - start_time < timeout:
|
|
199
|
+
task = self.get_task(task_id)
|
|
200
|
+
|
|
201
|
+
if task.status == "completed":
|
|
202
|
+
return Transcript(
|
|
203
|
+
text=task.result_text or "",
|
|
204
|
+
srt_content=task.srt_content,
|
|
205
|
+
task_id=task_id,
|
|
206
|
+
original_filename=task.original_filename,
|
|
207
|
+
audio_url=task.audio_url
|
|
208
|
+
)
|
|
209
|
+
elif task.status == "failed":
|
|
210
|
+
raise TranscriptionError(f"Transcription failed: {task.error}")
|
|
211
|
+
|
|
212
|
+
time.sleep(poll_interval)
|
|
213
|
+
|
|
214
|
+
raise TimeoutError(f"Task {task_id} did not complete within {timeout} seconds")
|
|
215
|
+
|
|
216
|
+
def list_tasks(self) -> List[TranscriptTask]:
|
|
217
|
+
"""
|
|
218
|
+
Get all transcription tasks for the current user.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of TranscriptTask objects
|
|
222
|
+
"""
|
|
223
|
+
response = self._make_request("GET", "/audio/tasks")
|
|
224
|
+
|
|
225
|
+
tasks = []
|
|
226
|
+
for task_data in response:
|
|
227
|
+
tasks.append(TranscriptTask(
|
|
228
|
+
task_id=task_data["task_id"],
|
|
229
|
+
status=task_data["status"],
|
|
230
|
+
original_filename=task_data["original_filename"],
|
|
231
|
+
audio_url=task_data.get("task_blob_directory"),
|
|
232
|
+
srt_requested=task_data["srt_requested"],
|
|
233
|
+
created_at=datetime.fromisoformat(task_data["created_at"].replace('Z', '+00:00'))
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
return tasks
|
|
237
|
+
|
|
238
|
+
def get_balance(self) -> Balance:
|
|
239
|
+
"""
|
|
240
|
+
Get the current user's balance.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Balance object with current balance information
|
|
244
|
+
"""
|
|
245
|
+
response = self._make_request("GET", "/billing/balance")
|
|
246
|
+
|
|
247
|
+
return Balance(
|
|
248
|
+
balance=response["balance"],
|
|
249
|
+
last_updated=datetime.fromisoformat(response["last_updated"].replace('Z', '+00:00'))
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def get_usage_history(
|
|
253
|
+
self,
|
|
254
|
+
start_date: Optional[datetime] = None,
|
|
255
|
+
end_date: Optional[datetime] = None,
|
|
256
|
+
page: int = 1,
|
|
257
|
+
page_size: int = 50
|
|
258
|
+
) -> UsageHistory:
|
|
259
|
+
"""
|
|
260
|
+
Get usage history for the current user.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
start_date: Start date for the history (default: 30 days ago)
|
|
264
|
+
end_date: End date for the history (default: now)
|
|
265
|
+
page: Page number (default: 1)
|
|
266
|
+
page_size: Number of records per page (default: 50)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
UsageHistory object with usage records
|
|
270
|
+
"""
|
|
271
|
+
params = {"page": page, "page_size": page_size}
|
|
272
|
+
if start_date:
|
|
273
|
+
params["start_date"] = start_date.isoformat()
|
|
274
|
+
if end_date:
|
|
275
|
+
params["end_date"] = end_date.isoformat()
|
|
276
|
+
|
|
277
|
+
response = self._make_request("GET", "/billing/usage-history", params=params)
|
|
278
|
+
|
|
279
|
+
records = []
|
|
280
|
+
for record_data in response["records"]:
|
|
281
|
+
records.append(UsageRecord(
|
|
282
|
+
id=record_data["id"],
|
|
283
|
+
service_type=record_data["service_type"],
|
|
284
|
+
usage_amount=record_data["total_audio_usage"],
|
|
285
|
+
cost=record_data["cost"],
|
|
286
|
+
timestamp=datetime.fromisoformat(record_data["timestamp"].replace('Z', '+00:00')),
|
|
287
|
+
api_key_id=record_data.get("api_key_id")
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
return UsageHistory(
|
|
291
|
+
records=records,
|
|
292
|
+
total_records=response["total_records"],
|
|
293
|
+
total_pages=response["total_pages"],
|
|
294
|
+
current_page=response["current_page"],
|
|
295
|
+
start_date=datetime.fromisoformat(response["start_date"].replace('Z', '+00:00')),
|
|
296
|
+
end_date=datetime.fromisoformat(response["end_date"].replace('Z', '+00:00')),
|
|
297
|
+
period_summary=response["period_summary"]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def get_daily_usage(
|
|
301
|
+
self,
|
|
302
|
+
start_date: Optional[date] = None,
|
|
303
|
+
end_date: Optional[date] = None,
|
|
304
|
+
page: int = 1,
|
|
305
|
+
page_size: int = 30
|
|
306
|
+
) -> DailyUsage:
|
|
307
|
+
"""
|
|
308
|
+
Get daily usage history for the current user.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
start_date: Start date for the history (default: 30 days ago)
|
|
312
|
+
end_date: End date for the history (default: today)
|
|
313
|
+
page: Page number (default: 1)
|
|
314
|
+
page_size: Number of records per page (default: 30)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
DailyUsage object with daily usage records
|
|
318
|
+
"""
|
|
319
|
+
params = {"page": page, "page_size": page_size}
|
|
320
|
+
if start_date:
|
|
321
|
+
params["start_date"] = start_date.isoformat()
|
|
322
|
+
if end_date:
|
|
323
|
+
params["end_date"] = end_date.isoformat()
|
|
324
|
+
|
|
325
|
+
response = self._make_request("GET", "/billing/daily-usage", params=params)
|
|
326
|
+
|
|
327
|
+
daily_records = []
|
|
328
|
+
for record_data in response["records"]:
|
|
329
|
+
daily_records.append(DailyUsageRecord(
|
|
330
|
+
date=date.fromisoformat(record_data["date"]),
|
|
331
|
+
total_cost=record_data["total_cost"],
|
|
332
|
+
total_audio_usage=record_data["transcription_usage"],
|
|
333
|
+
record_count=1, # Each record represents one day
|
|
334
|
+
transcription_usage=record_data.get("transcription_usage", 0.0),
|
|
335
|
+
transcription_cost=record_data.get("transcription_cost", 0.0),
|
|
336
|
+
translation_usage=record_data.get("translation_usage", 0.0),
|
|
337
|
+
translation_cost=record_data.get("translation_cost", 0.0),
|
|
338
|
+
summarization_usage=record_data.get("summarization_usage", 0.0),
|
|
339
|
+
summarization_cost=record_data.get("summarization_cost", 0.0)
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
return DailyUsage(
|
|
343
|
+
daily_records=daily_records,
|
|
344
|
+
total_records=response["total_records"],
|
|
345
|
+
total_pages=response["total_pages"],
|
|
346
|
+
current_page=response["current_page"],
|
|
347
|
+
start_date=date.fromisoformat(response["start_date"]),
|
|
348
|
+
end_date=date.fromisoformat(response["end_date"]),
|
|
349
|
+
total_cost=response["period_summary"]["total_cost"],
|
|
350
|
+
total_audio_seconds=response["period_summary"]["total_transcription_usage"]
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def get_user(self) -> User:
|
|
354
|
+
"""
|
|
355
|
+
Get current user details.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
User object with user information
|
|
359
|
+
"""
|
|
360
|
+
response = self._make_request("GET", "/user/")
|
|
361
|
+
|
|
362
|
+
return User(
|
|
363
|
+
id=response["id"],
|
|
364
|
+
email=response["email"],
|
|
365
|
+
first_name=response["first_name"],
|
|
366
|
+
last_name=response["last_name"],
|
|
367
|
+
is_verified=response["is_verified"]
|
|
368
|
+
)
|
orbitalsai/exceptions.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI SDK Exceptions
|
|
3
|
+
|
|
4
|
+
Custom exceptions for the OrbitalsAI Python SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OrbitalsAIError(Exception):
|
|
9
|
+
"""Base exception for all OrbitalsAI SDK errors."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(OrbitalsAIError):
|
|
14
|
+
"""Raised when API key authentication fails."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InsufficientBalanceError(OrbitalsAIError):
|
|
19
|
+
"""Raised when user has insufficient balance for the operation."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FileNotFoundError(OrbitalsAIError):
|
|
24
|
+
"""Raised when the specified audio file is not found."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UnsupportedFileError(OrbitalsAIError):
|
|
29
|
+
"""Raised when the audio file format is not supported."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UnsupportedLanguageError(OrbitalsAIError):
|
|
34
|
+
"""Raised when the specified language is not supported."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TaskNotFoundError(OrbitalsAIError):
|
|
39
|
+
"""Raised when the specified task ID is not found."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TranscriptionError(OrbitalsAIError):
|
|
44
|
+
"""Raised when transcription processing fails."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TimeoutError(OrbitalsAIError):
|
|
49
|
+
"""Raised when an operation times out."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class APIError(OrbitalsAIError):
|
|
54
|
+
"""Raised when the API returns an error response."""
|
|
55
|
+
def __init__(self, message: str, status_code: int = None, response_data: dict = None):
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.status_code = status_code
|
|
58
|
+
self.response_data = response_data
|
orbitalsai/models.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI SDK Data Models
|
|
3
|
+
|
|
4
|
+
Data models for API requests and responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
from datetime import datetime, date
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TranscriptTask:
|
|
14
|
+
"""Represents a transcription task status."""
|
|
15
|
+
task_id: int
|
|
16
|
+
status: str
|
|
17
|
+
original_filename: str
|
|
18
|
+
audio_url: Optional[str] = None
|
|
19
|
+
srt_requested: bool = False
|
|
20
|
+
result_text: Optional[str] = None
|
|
21
|
+
srt_content: Optional[str] = None
|
|
22
|
+
error: Optional[str] = None
|
|
23
|
+
created_at: Optional[datetime] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Transcript:
|
|
28
|
+
"""Represents a completed transcription result."""
|
|
29
|
+
text: str
|
|
30
|
+
srt_content: Optional[str] = None
|
|
31
|
+
task_id: int = None
|
|
32
|
+
original_filename: str = None
|
|
33
|
+
audio_url: Optional[str] = None
|
|
34
|
+
processing_time: Optional[float] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Balance:
|
|
39
|
+
"""Represents user balance information."""
|
|
40
|
+
balance: float
|
|
41
|
+
last_updated: datetime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class UsageRecord:
|
|
46
|
+
"""Represents a single usage record."""
|
|
47
|
+
id: int
|
|
48
|
+
service_type: str
|
|
49
|
+
usage_amount: float
|
|
50
|
+
cost: float
|
|
51
|
+
timestamp: datetime
|
|
52
|
+
api_key_id: Optional[int] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class UsageHistory:
|
|
57
|
+
"""Represents usage history response."""
|
|
58
|
+
records: List[UsageRecord]
|
|
59
|
+
total_records: int
|
|
60
|
+
total_pages: int
|
|
61
|
+
current_page: int
|
|
62
|
+
start_date: datetime
|
|
63
|
+
end_date: datetime
|
|
64
|
+
period_summary: dict
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class DailyUsageRecord:
|
|
69
|
+
"""Represents daily usage record."""
|
|
70
|
+
date: date
|
|
71
|
+
total_cost: float
|
|
72
|
+
total_audio_usage: float
|
|
73
|
+
record_count: int
|
|
74
|
+
transcription_usage: float = 0.0
|
|
75
|
+
transcription_cost: float = 0.0
|
|
76
|
+
translation_usage: float = 0.0
|
|
77
|
+
translation_cost: float = 0.0
|
|
78
|
+
summarization_usage: float = 0.0
|
|
79
|
+
summarization_cost: float = 0.0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class DailyUsage:
|
|
84
|
+
"""Represents daily usage response."""
|
|
85
|
+
daily_records: List[DailyUsageRecord]
|
|
86
|
+
total_records: int
|
|
87
|
+
total_pages: int
|
|
88
|
+
current_page: int
|
|
89
|
+
start_date: date
|
|
90
|
+
end_date: date
|
|
91
|
+
total_cost: float
|
|
92
|
+
total_audio_seconds: float
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class User:
|
|
97
|
+
"""Represents user information."""
|
|
98
|
+
id: int
|
|
99
|
+
email: str
|
|
100
|
+
first_name: str
|
|
101
|
+
last_name: str
|
|
102
|
+
is_verified: bool
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class APIKey:
|
|
107
|
+
"""Represents API key information."""
|
|
108
|
+
id: int
|
|
109
|
+
name: str
|
|
110
|
+
key_prefix: str
|
|
111
|
+
permissions: str
|
|
112
|
+
is_active: bool
|
|
113
|
+
created_at: datetime
|
|
114
|
+
last_used: Optional[datetime] = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Supported languages and file formats
|
|
118
|
+
SUPPORTED_LANGUAGES = [
|
|
119
|
+
"english", "hausa", "igbo", "yoruba", "swahili", "pidgin", "kinyarwanda"
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
SUPPORTED_AUDIO_FORMATS = [
|
|
123
|
+
".wav", ".wave", ".mp3", ".mpeg", ".ogg", ".oga", ".opus",
|
|
124
|
+
".flac", ".aac", ".m4a", ".wma", ".amr", ".3gp"
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
SUPPORTED_AUDIO_MIMETYPES = [
|
|
128
|
+
"audio/wav", "audio/wave", "audio/x-wav", "audio/vnd.wave",
|
|
129
|
+
"audio/mp3", "audio/mpeg", "audio/mpeg3", "audio/x-mpeg-3",
|
|
130
|
+
"audio/ogg", "audio/vorbis", "audio/x-vorbis+ogg",
|
|
131
|
+
"audio/opus", "audio/flac", "audio/x-flac",
|
|
132
|
+
"audio/aac", "audio/x-aac", "audio/mp4", "audio/m4a", "audio/x-m4a",
|
|
133
|
+
"audio/x-ms-wma", "audio/amr", "audio/3gpp", "audio/3gpp2",
|
|
134
|
+
"application/octet-stream"
|
|
135
|
+
]
|
orbitalsai/utils.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI SDK Utilities
|
|
3
|
+
|
|
4
|
+
Helper utilities for the OrbitalsAI Python SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import mimetypes
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .models import SUPPORTED_AUDIO_FORMATS, SUPPORTED_AUDIO_MIMETYPES, SUPPORTED_LANGUAGES
|
|
13
|
+
from .exceptions import UnsupportedFileError, UnsupportedLanguageError, FileNotFoundError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_audio_file(file_path: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Validate that the audio file exists and is in a supported format.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
file_path: Path to the audio file
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
FileNotFoundError: If the file doesn't exist
|
|
25
|
+
UnsupportedFileError: If the file format is not supported
|
|
26
|
+
"""
|
|
27
|
+
if not os.path.exists(file_path):
|
|
28
|
+
raise FileNotFoundError(f"Audio file not found: {file_path}")
|
|
29
|
+
|
|
30
|
+
file_path = Path(file_path)
|
|
31
|
+
file_extension = file_path.suffix.lower()
|
|
32
|
+
|
|
33
|
+
# Check file extension
|
|
34
|
+
if file_extension not in SUPPORTED_AUDIO_FORMATS:
|
|
35
|
+
raise UnsupportedFileError(
|
|
36
|
+
f"Unsupported file format: {file_extension}. "
|
|
37
|
+
f"Supported formats: {', '.join(SUPPORTED_AUDIO_FORMATS)}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Check MIME type
|
|
41
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
42
|
+
if mime_type and mime_type not in SUPPORTED_AUDIO_MIMETYPES:
|
|
43
|
+
# Only raise error if MIME type is detected and not supported
|
|
44
|
+
# Some files might not have MIME type detection but still be valid
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_language(language: str) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Validate that the language is supported.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
language: Language code
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
UnsupportedLanguageError: If the language is not supported
|
|
57
|
+
"""
|
|
58
|
+
if language.lower() not in SUPPORTED_LANGUAGES:
|
|
59
|
+
raise UnsupportedLanguageError(
|
|
60
|
+
f"Unsupported language: {language}. "
|
|
61
|
+
f"Supported languages: {', '.join(SUPPORTED_LANGUAGES)}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_file_size(file_path: str) -> int:
|
|
66
|
+
"""
|
|
67
|
+
Get the size of a file in bytes.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
file_path: Path to the file
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
File size in bytes
|
|
74
|
+
"""
|
|
75
|
+
return os.path.getsize(file_path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def format_duration(seconds: float) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Format duration in seconds to human-readable string.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
seconds: Duration in seconds
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Formatted duration string (e.g., "2m 30s")
|
|
87
|
+
"""
|
|
88
|
+
if seconds < 60:
|
|
89
|
+
return f"{seconds:.1f}s"
|
|
90
|
+
elif seconds < 3600:
|
|
91
|
+
minutes = int(seconds // 60)
|
|
92
|
+
remaining_seconds = seconds % 60
|
|
93
|
+
return f"{minutes}m {remaining_seconds:.1f}s"
|
|
94
|
+
else:
|
|
95
|
+
hours = int(seconds // 3600)
|
|
96
|
+
minutes = int((seconds % 3600) // 60)
|
|
97
|
+
remaining_seconds = seconds % 60
|
|
98
|
+
return f"{hours}h {minutes}m {remaining_seconds:.1f}s"
|