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/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
+ )
@@ -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"