sarvamai 0.1.13a2__py3-none-any.whl → 0.1.15__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.
@@ -0,0 +1,497 @@
1
+ import asyncio
2
+ import mimetypes
3
+ import os
4
+ import time
5
+ import typing
6
+ import httpx
7
+ from http import HTTPStatus
8
+
9
+ from ..types import JobStatusV1Response
10
+
11
+ if typing.TYPE_CHECKING:
12
+ from .client import AsyncSpeechToTextJobClient, SpeechToTextJobClient
13
+
14
+
15
+ class AsyncSpeechToTextJob:
16
+ def __init__(self, job_id: str, client: "AsyncSpeechToTextJobClient"):
17
+ """
18
+ Initialize the asynchronous speech-to-text job.
19
+
20
+ Parameters
21
+ ----------
22
+ job_id : str
23
+ The unique job identifier returned from a previous job initialization.
24
+
25
+ client : AsyncSpeechToTextJobClient
26
+ The async client instance used to create the job.
27
+
28
+ !!! important
29
+ This must be the **same client instance** that was used to initialize
30
+ the job originally, as it contains the subscription key and configuration
31
+ required to authenticate and manage the job.
32
+
33
+ """
34
+ self._job_id = job_id
35
+ self._client = client
36
+
37
+ @property
38
+ def job_id(self) -> str:
39
+ """
40
+ Returns the job ID associated with this job instance.
41
+
42
+ Returns
43
+ -------
44
+ str
45
+ """
46
+ return self._job_id
47
+
48
+ async def upload_files(
49
+ self, file_paths: typing.Sequence[str], timeout: float = 60.0
50
+ ) -> bool:
51
+ """
52
+ Upload input audio files for the speech-to-text job.
53
+
54
+ Parameters
55
+ ----------
56
+ file_paths : Sequence[str]
57
+ List of full paths to local audio files.
58
+
59
+ timeout : float, optional
60
+ The maximum time to wait for the upload to complete (in seconds),
61
+ by default 60.0
62
+ Returns
63
+ -------
64
+ bool
65
+ True if all files are uploaded successfully.
66
+ """
67
+ upload_links = await self._client.get_upload_links(
68
+ job_id=self._job_id,
69
+ files=[os.path.basename(p) for p in file_paths],
70
+ )
71
+ client_timeout = httpx.Timeout(timeout=timeout)
72
+ async with httpx.AsyncClient(timeout=client_timeout) as session:
73
+ for path in file_paths:
74
+ file_name = os.path.basename(path)
75
+ url = upload_links.upload_urls[file_name].file_url
76
+ with open(path, "rb") as f:
77
+ content_type, _ = mimetypes.guess_type(path)
78
+ if content_type is None:
79
+ content_type = "audio/wav"
80
+ response = await session.put(
81
+ url,
82
+ content=f.read(),
83
+ headers={
84
+ "x-ms-blob-type": "BlockBlob",
85
+ "Content-Type": content_type,
86
+ },
87
+ )
88
+ if (
89
+ response.status_code > HTTPStatus.IM_USED
90
+ or response.status_code < HTTPStatus.OK
91
+ ):
92
+ raise RuntimeError(
93
+ f"Upload failed for {file_name}: {response.status_code}"
94
+ )
95
+ return True
96
+
97
+ async def wait_until_complete(
98
+ self, poll_interval: int = 5, timeout: int = 600
99
+ ) -> JobStatusV1Response:
100
+ """
101
+ Polls job status until it completes or fails.
102
+
103
+ Parameters
104
+ ----------
105
+ poll_interval : int, optional
106
+ Time in seconds between polling attempts (default is 5).
107
+
108
+ timeout : int, optional
109
+ Maximum time to wait for completion in seconds (default is 600).
110
+
111
+ Returns
112
+ -------
113
+ JobStatusV1Response
114
+ Final job status.
115
+
116
+ Raises
117
+ ------
118
+ TimeoutError
119
+ If the job does not complete within the given timeout.
120
+ """
121
+ start = asyncio.get_event_loop().time()
122
+ while True:
123
+ status = await self.get_status()
124
+ state = status.job_state.lower()
125
+ if state in {"completed", "failed"}:
126
+ return status
127
+ if asyncio.get_event_loop().time() - start > timeout:
128
+ raise TimeoutError(
129
+ f"Job {self._job_id} did not complete within {timeout} seconds."
130
+ )
131
+ await asyncio.sleep(poll_interval)
132
+
133
+ async def get_output_mappings(self) -> typing.List[typing.Dict[str, str]]:
134
+ """
135
+ Get the mapping of input files to their corresponding output files.
136
+
137
+ Returns
138
+ -------
139
+ List[Dict[str, str]]
140
+ List of mappings with keys 'input_file' and 'output_file'.
141
+ """
142
+ job_status = await self.get_status()
143
+ return [
144
+ {
145
+ "input_file": detail.inputs[0].file_name,
146
+ "output_file": detail.outputs[0].file_name,
147
+ }
148
+ for detail in (job_status.job_details or [])
149
+ if detail.inputs and detail.outputs
150
+ ]
151
+
152
+ async def download_outputs(self, output_dir: str) -> bool:
153
+ """
154
+ Download output files to the specified directory.
155
+
156
+ Parameters
157
+ ----------
158
+ output_dir : str
159
+ Local directory where outputs will be saved.
160
+
161
+ Returns
162
+ -------
163
+ bool
164
+ True if all files downloaded successfully.
165
+
166
+ Raises
167
+ ------
168
+ RuntimeError
169
+ If a file fails to download.
170
+ """
171
+ mappings = await self.get_output_mappings()
172
+ file_names = [m["output_file"] for m in mappings]
173
+ download_links = await self._client.get_download_links(
174
+ job_id=self._job_id, files=file_names
175
+ )
176
+
177
+ os.makedirs(output_dir, exist_ok=True)
178
+ async with httpx.AsyncClient() as session:
179
+ for m in mappings:
180
+ url = download_links.download_urls[m["output_file"]].file_url
181
+ response = await session.get(url)
182
+ if (
183
+ response.status_code > HTTPStatus.IM_USED
184
+ or response.status_code < HTTPStatus.OK
185
+ ):
186
+ raise RuntimeError(
187
+ f"Download failed for {m['output_file']}: {response.status_code}"
188
+ )
189
+ output_path = os.path.join(output_dir, f"{m['input_file']}.json")
190
+ with open(output_path, "wb") as f:
191
+ f.write(response.content)
192
+ return True
193
+
194
+ async def get_status(self) -> JobStatusV1Response:
195
+ """
196
+ Retrieve the current status of the job.
197
+
198
+ Returns
199
+ -------
200
+ JobStatusV1Response
201
+ """
202
+ return await self._client.get_status(self._job_id)
203
+
204
+ async def start(self) -> JobStatusV1Response:
205
+ """
206
+ Start the speech-to-text job processing.
207
+
208
+ Returns
209
+ -------
210
+ JobStatusV1Response
211
+ """
212
+ return await self._client.start(job_id=self._job_id)
213
+
214
+ async def exists(self) -> bool:
215
+ """
216
+ Check if the job exists in the system.
217
+
218
+ Returns
219
+ -------
220
+ bool
221
+ """
222
+ try:
223
+ await self.get_status()
224
+ return True
225
+ except httpx.HTTPStatusError:
226
+ return False
227
+
228
+ async def is_complete(self) -> bool:
229
+ """
230
+ Check if the job is either completed or failed.
231
+
232
+ Returns
233
+ -------
234
+ bool
235
+ """
236
+ state = (await self.get_status()).job_state.lower()
237
+ return state in {"completed", "failed"}
238
+
239
+ async def is_successful(self) -> bool:
240
+ """
241
+ Check if the job completed successfully.
242
+
243
+ Returns
244
+ -------
245
+ bool
246
+ """
247
+ return (await self.get_status()).job_state.lower() == "completed"
248
+
249
+ async def is_failed(self) -> bool:
250
+ """
251
+ Check if the job has failed.
252
+
253
+ Returns
254
+ -------
255
+ bool
256
+ """
257
+ return (await self.get_status()).job_state.lower() == "failed"
258
+
259
+
260
+ class SpeechToTextJob:
261
+ def __init__(self, job_id: str, client: "SpeechToTextJobClient"):
262
+ """
263
+ Initialize the synchronous speech-to-text job.
264
+
265
+ Parameters
266
+ ----------
267
+ job_id : str
268
+ The unique job identifier returned from a previous job initialization.
269
+
270
+ client : SpeechToTextJobClient
271
+ The client instance used to create the job.
272
+
273
+ !!! important
274
+ This must be the **same client instance** that was used to initialize
275
+ the job originally, as it contains the subscription key and configuration
276
+ required to authenticate and manage the job.
277
+
278
+ """
279
+ self._job_id = job_id
280
+ self._client = client
281
+
282
+ @property
283
+ def job_id(self) -> str:
284
+ """
285
+ Returns the job ID associated with this job instance.
286
+
287
+ Returns
288
+ -------
289
+ str
290
+ """
291
+ return self._job_id
292
+
293
+ def upload_files(
294
+ self, file_paths: typing.Sequence[str], timeout: float = 60.0
295
+ ) -> bool:
296
+ """
297
+ Upload input audio files for the speech-to-text job.
298
+
299
+ Parameters
300
+ ----------
301
+ file_paths : Sequence[str]
302
+ List of full paths to local audio files.
303
+
304
+ timeout : float, optional
305
+ The maximum time to wait for the upload to complete (in seconds),
306
+ by default 60.0
307
+ Returns
308
+ -------
309
+ bool
310
+ True if all files are uploaded successfully.
311
+ """
312
+ upload_links = self._client.get_upload_links(
313
+ job_id=self._job_id, files=[os.path.basename(p) for p in file_paths]
314
+ )
315
+ client_timeout = httpx.Timeout(timeout=timeout)
316
+ with httpx.Client(timeout=client_timeout) as client:
317
+ for path in file_paths:
318
+ file_name = os.path.basename(path)
319
+ url = upload_links.upload_urls[file_name].file_url
320
+ with open(path, "rb") as f:
321
+ response = client.put(
322
+ url,
323
+ content=f,
324
+ headers={
325
+ "x-ms-blob-type": "BlockBlob",
326
+ "Content-Type": "audio/wav",
327
+ },
328
+ )
329
+ if (
330
+ response.status_code > HTTPStatus.IM_USED
331
+ or response.status_code < HTTPStatus.OK
332
+ ):
333
+ raise RuntimeError(
334
+ f"Upload failed for {file_name}: {response.status_code}"
335
+ )
336
+ return True
337
+
338
+ def wait_until_complete(
339
+ self, poll_interval: int = 5, timeout: int = 600
340
+ ) -> JobStatusV1Response:
341
+ """
342
+ Polls job status until it completes or fails.
343
+
344
+ Parameters
345
+ ----------
346
+ poll_interval : int, optional
347
+ Time in seconds between polling attempts (default is 5).
348
+
349
+ timeout : int, optional
350
+ Maximum time to wait for completion in seconds (default is 600).
351
+
352
+ Returns
353
+ -------
354
+ JobStatusV1Response
355
+ Final job status.
356
+
357
+ Raises
358
+ ------
359
+ TimeoutError
360
+ If the job does not complete within the given timeout.
361
+ """
362
+ start = time.monotonic()
363
+ while True:
364
+ status = self.get_status()
365
+ state = status.job_state.lower()
366
+ if state in {"completed", "failed"}:
367
+ return status
368
+ if time.monotonic() - start > timeout:
369
+ raise TimeoutError(
370
+ f"Job {self._job_id} did not complete within {timeout} seconds."
371
+ )
372
+ time.sleep(poll_interval)
373
+
374
+ def get_output_mappings(self) -> typing.List[typing.Dict[str, str]]:
375
+ """
376
+ Get the mapping of input files to their corresponding output files.
377
+
378
+ Returns
379
+ -------
380
+ List[Dict[str, str]]
381
+ List of mappings with keys 'input_file' and 'output_file'.
382
+ """
383
+ job_status = self.get_status()
384
+ return [
385
+ {
386
+ "input_file": detail.inputs[0].file_name,
387
+ "output_file": detail.outputs[0].file_name,
388
+ }
389
+ for detail in (job_status.job_details or [])
390
+ if detail.inputs and detail.outputs
391
+ ]
392
+
393
+ def download_outputs(self, output_dir: str) -> bool:
394
+ """
395
+ Download output files to the specified directory.
396
+
397
+ Parameters
398
+ ----------
399
+ output_dir : str
400
+ Local directory where outputs will be saved.
401
+
402
+ Returns
403
+ -------
404
+ bool
405
+ True if all files downloaded successfully.
406
+
407
+ Raises
408
+ ------
409
+ RuntimeError
410
+ If a file fails to download.
411
+ """
412
+ mappings = self.get_output_mappings()
413
+ file_names = [m["output_file"] for m in mappings]
414
+ download_links = self._client.get_download_links(
415
+ job_id=self._job_id, files=file_names
416
+ )
417
+
418
+ os.makedirs(output_dir, exist_ok=True)
419
+ with httpx.Client() as client:
420
+ for m in mappings:
421
+ url = download_links.download_urls[m["output_file"]].file_url
422
+ response = client.get(url)
423
+ if (
424
+ response.status_code > HTTPStatus.IM_USED
425
+ or response.status_code < HTTPStatus.OK
426
+ ):
427
+ raise RuntimeError(
428
+ f"Download failed for {m['output_file']}: {response.status_code}"
429
+ )
430
+ output_path = os.path.join(output_dir, f"{m['input_file']}.json")
431
+ with open(output_path, "wb") as f:
432
+ f.write(response.content)
433
+ return True
434
+
435
+ def get_status(self) -> JobStatusV1Response:
436
+ """
437
+ Retrieve the current status of the job.
438
+
439
+ Returns
440
+ -------
441
+ JobStatusV1Response
442
+ """
443
+ return self._client.get_status(self._job_id)
444
+
445
+ def start(self) -> JobStatusV1Response:
446
+ """
447
+ Start the speech-to-text job processing.
448
+
449
+ Returns
450
+ -------
451
+ JobStatusV1Response
452
+ """
453
+ return self._client.start(job_id=self._job_id)
454
+
455
+ def exists(self) -> bool:
456
+ """
457
+ Check if the job exists in the system.
458
+
459
+ Returns
460
+ -------
461
+ bool
462
+ """
463
+ try:
464
+ self.get_status()
465
+ return True
466
+ except httpx.HTTPStatusError:
467
+ return False
468
+
469
+ def is_complete(self) -> bool:
470
+ """
471
+ Check if the job is either completed or failed.
472
+
473
+ Returns
474
+ -------
475
+ bool
476
+ """
477
+ return self.get_status().job_state.lower() in {"completed", "failed"}
478
+
479
+ def is_successful(self) -> bool:
480
+ """
481
+ Check if the job completed successfully.
482
+
483
+ Returns
484
+ -------
485
+ bool
486
+ """
487
+ return self.get_status().job_state.lower() == "completed"
488
+
489
+ def is_failed(self) -> bool:
490
+ """
491
+ Check if the job has failed.
492
+
493
+ Returns
494
+ -------
495
+ bool
496
+ """
497
+ return self.get_status().job_state.lower() == "failed"
@@ -10,7 +10,9 @@ from ..types.bulk_job_init_response_v_1 import BulkJobInitResponseV1
10
10
  from ..types.files_download_response import FilesDownloadResponse
11
11
  from ..types.files_upload_response import FilesUploadResponse
12
12
  from ..types.job_status_v_1_response import JobStatusV1Response
13
+ from ..types.speech_to_text_translate_model import SpeechToTextTranslateModel
13
14
  from .raw_client import AsyncRawSpeechToTextTranslateJobClient, RawSpeechToTextTranslateJobClient
15
+ from .job import AsyncSpeechToTextTranslateJob, SpeechToTextTranslateJob
14
16
 
15
17
  # this is used as the default value for optional parameters
16
18
  OMIT = typing.cast(typing.Any, ...)
@@ -236,6 +238,72 @@ class SpeechToTextTranslateJobClient:
236
238
  )
237
239
  return _response.data
238
240
 
241
+ def create_job(
242
+ self,
243
+ model: SpeechToTextTranslateModel = "saaras:v2.5",
244
+ with_diarization: bool = False,
245
+ prompt: typing.Optional[str] = None,
246
+ num_speakers: typing.Optional[int] = None,
247
+ callback: typing.Optional[BulkJobCallbackParams] = OMIT,
248
+ request_options: typing.Optional[RequestOptions] = None,
249
+ ) -> SpeechToTextTranslateJob:
250
+ """
251
+ Create a new Speech-to-Text-Translate bulk job.
252
+
253
+ Parameters
254
+ ----------
255
+ model : typing.Optional[SpeechToTextTranslateModel], default="saaras:v2.5"
256
+ The model to use for speech-to-text translation.
257
+
258
+ with_diarization : typing.Optional[bool], default=False
259
+ Whether to enable speaker diarization (i.e., distinguishing who is speaking).
260
+
261
+ prompt : typing.Optional[str], default=None
262
+ An optional prompt to guide the transcription and translation model.
263
+
264
+ num_speakers : typing.Optional[int], default=None
265
+ The number of distinct speakers in the input audio, if known.
266
+
267
+ callback : typing.Optional[BulkJobCallbackParams], default=OMIT
268
+ Optional callback configuration to receive job completion events via webhook.
269
+
270
+ request_options : typing.Optional[RequestOptions], default=None
271
+ Optional configuration for request timeout, retries, etc.
272
+
273
+ Returns
274
+ -------
275
+ SpeechToTextTranslateJob
276
+ A handle to the newly created Speech-to-Text-Translate job.
277
+ """
278
+ response = self.initialise(
279
+ job_parameters=SpeechToTextTranslateJobParametersParams(
280
+ prompt=prompt, # type: ignore[typeddict-item]
281
+ model=model,
282
+ with_diarization=with_diarization,
283
+ num_speakers=num_speakers, # type: ignore[typeddict-item]
284
+ ),
285
+ callback=callback,
286
+ request_options=request_options,
287
+ )
288
+ return SpeechToTextTranslateJob(job_id=response.job_id, client=self)
289
+
290
+ def get_job(self, job_id: str) -> SpeechToTextTranslateJob:
291
+ """
292
+ Get an existing Speech-to-Text-Translate job handle by job ID.
293
+
294
+ Parameters
295
+ ----------
296
+ job_id : str
297
+ The job ID of the previously created Speech-to-Text-Translate job.
298
+
299
+ Returns
300
+ -------
301
+ SpeechToTextTranslateJob
302
+ A job handle which can be used to check status or retrieve results.
303
+ """
304
+ return SpeechToTextTranslateJob(job_id=job_id, client=self)
305
+
306
+
239
307
 
240
308
  class AsyncSpeechToTextTranslateJobClient:
241
309
  def __init__(self, *, client_wrapper: AsyncClientWrapper):
@@ -496,3 +564,68 @@ class AsyncSpeechToTextTranslateJobClient:
496
564
  job_id=job_id, files=files, ptu_id=ptu_id, request_options=request_options
497
565
  )
498
566
  return _response.data
567
+
568
+ async def create_job(
569
+ self,
570
+ model: SpeechToTextTranslateModel = "saaras:v2.5",
571
+ with_diarization: bool = False,
572
+ prompt: typing.Optional[str] = None,
573
+ num_speakers: typing.Optional[int] = None,
574
+ callback: typing.Optional[BulkJobCallbackParams] = OMIT,
575
+ request_options: typing.Optional[RequestOptions] = None,
576
+ ) -> "AsyncSpeechToTextTranslateJob":
577
+ """
578
+ Create a new Speech-to-Text-Translate bulk job.
579
+
580
+ Parameters
581
+ ----------
582
+ model : typing.Optional[SpeechToTextTranslateModel], default="saaras:v2.5"
583
+ The model to use for speech-to-text translation.
584
+
585
+ with_diarization : typing.Optional[bool], default=False
586
+ Whether to enable speaker diarization (i.e., distinguishing who is speaking).
587
+
588
+ prompt : typing.Optional[str], default=None
589
+ An optional prompt to guide the transcription and translation model.
590
+
591
+ num_speakers : typing.Optional[int], default=None
592
+ The number of distinct speakers in the input audio, if known.
593
+
594
+ callback : typing.Optional[BulkJobCallbackParams], default=OMIT
595
+ Optional callback configuration to receive job completion events via webhook.
596
+
597
+ request_options : typing.Optional[RequestOptions], default=None
598
+ Optional configuration for request timeout, retries, etc.
599
+
600
+ Returns
601
+ -------
602
+ AsyncSpeechToTextTranslateJob
603
+ A handle to the newly created job.
604
+ """
605
+ response = await self.initialise(
606
+ job_parameters=SpeechToTextTranslateJobParametersParams(
607
+ prompt=prompt, # type: ignore[typeddict-item]
608
+ model=model,
609
+ with_diarization=with_diarization, # type: ignore[typeddict-item]
610
+ num_speakers=num_speakers, # type: ignore[typeddict-item]
611
+ ),
612
+ callback=callback,
613
+ request_options=request_options,
614
+ )
615
+ return AsyncSpeechToTextTranslateJob(job_id=response.job_id, client=self)
616
+
617
+ async def get_job(self, job_id: str) -> "AsyncSpeechToTextTranslateJob":
618
+ """
619
+ Get an existing Speech-to-Text-Translate job handle by job ID.
620
+
621
+ Parameters
622
+ ----------
623
+ job_id : str
624
+ The job ID of the previously created speech-to-text-translate job.
625
+
626
+ Returns
627
+ -------
628
+ AsyncSpeechToTextTranslateJob
629
+ A job handle which can be used to check status or retrieve results.
630
+ """
631
+ return AsyncSpeechToTextTranslateJob(job_id=job_id, client=self)