sarvamai 0.1.13a2__py3-none-any.whl → 0.1.14__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.
@@ -23,10 +23,10 @@ class BaseClientWrapper:
23
23
 
24
24
  def get_headers(self) -> typing.Dict[str, str]:
25
25
  headers: typing.Dict[str, str] = {
26
- "User-Agent": "sarvamai/0.1.13a2",
26
+ "User-Agent": "sarvamai/0.1.14",
27
27
  "X-Fern-Language": "Python",
28
28
  "X-Fern-SDK-Name": "sarvamai",
29
- "X-Fern-SDK-Version": "0.1.13a2",
29
+ "X-Fern-SDK-Version": "0.1.14",
30
30
  **(self.get_custom_headers() or {}),
31
31
  }
32
32
  headers["api-subscription-key"] = self.api_subscription_key
@@ -10,7 +10,10 @@ 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_model import SpeechToTextModel
14
+ from ..types.speech_to_text_language import SpeechToTextLanguage
13
15
  from .raw_client import AsyncRawSpeechToTextJobClient, RawSpeechToTextJobClient
16
+ from .job import AsyncSpeechToTextJob, SpeechToTextJob
14
17
 
15
18
  # this is used as the default value for optional parameters
16
19
  OMIT = typing.cast(typing.Any, ...)
@@ -215,6 +218,76 @@ class SpeechToTextJobClient:
215
218
  _response = self._raw_client.get_download_links(job_id=job_id, files=files, request_options=request_options)
216
219
  return _response.data
217
220
 
221
+ def create_job(
222
+ self,
223
+ model: SpeechToTextModel = "saarika:v2.5",
224
+ with_diarization: bool = False,
225
+ with_timestamps: bool = False,
226
+ language_code: typing.Optional[SpeechToTextLanguage] = None,
227
+ num_speakers: typing.Optional[int] = None,
228
+ callback: typing.Optional[BulkJobCallbackParams] = OMIT,
229
+ request_options: typing.Optional[RequestOptions] = None,
230
+ ) -> SpeechToTextJob:
231
+ """
232
+ Create a new Speech-to-Text bulk job.
233
+
234
+ Parameters
235
+ ----------
236
+ model : SpeechToTextModel, default="saarika:v2.5"
237
+ The model to use for transcription.
238
+
239
+ with_diarization : typing.Optional[bool], default=False
240
+ Whether to enable speaker diarization (distinguishing who said what).
241
+
242
+ with_timestamps : typing.Optional[bool], default=False
243
+ Whether to include word-level timestamps in the transcription output.
244
+
245
+ language_code : typing.Optional[SpeechToTextLanguage], default=None
246
+ The language code of the input audio (e.g., "hi-IN", "bn-IN").
247
+
248
+ num_speakers : typing.Optional[int], default=None
249
+ The number of distinct speakers in the audio, if known.
250
+
251
+ callback : typing.Optional[BulkJobCallbackParams], default=OMIT
252
+ Optional callback configuration to receive job completion events.
253
+
254
+ request_options : typing.Optional[RequestOptions], default=None
255
+ Request-specific configuration.
256
+
257
+ Returns
258
+ -------
259
+ SpeechToTextJob
260
+ A handle to the newly created Speech-to-Text job.
261
+ """
262
+ response = self.initialise(
263
+ job_parameters=SpeechToTextJobParametersParams(
264
+ language_code=language_code,
265
+ model=model,
266
+ num_speakers=num_speakers, # type: ignore[typeddict-item]
267
+ with_diarization=with_diarization,
268
+ with_timestamps=with_timestamps,
269
+ ),
270
+ callback=callback,
271
+ request_options=request_options,
272
+ )
273
+ return SpeechToTextJob(job_id=response.job_id, client=self)
274
+
275
+ def get_job(self, job_id: str) -> SpeechToTextJob:
276
+ """
277
+ Get an existing Speech-to-Text job handle by job ID.
278
+
279
+ Parameters
280
+ ----------
281
+ job_id : str
282
+ The job ID of the previously created Speech-to-Text job.
283
+
284
+ Returns
285
+ -------
286
+ SpeechToTextJob
287
+ A job handle which can be used to check status or retrieve results.
288
+ """
289
+ return SpeechToTextJob(job_id=job_id, client=self)
290
+
218
291
 
219
292
  class AsyncSpeechToTextJobClient:
220
293
  def __init__(self, *, client_wrapper: AsyncClientWrapper):
@@ -456,3 +529,73 @@ class AsyncSpeechToTextJobClient:
456
529
  job_id=job_id, files=files, request_options=request_options
457
530
  )
458
531
  return _response.data
532
+
533
+ async def create_job(
534
+ self,
535
+ model: SpeechToTextModel = "saarika:v2.5",
536
+ with_diarization: bool = False,
537
+ with_timestamps: bool = False,
538
+ language_code: typing.Optional[SpeechToTextLanguage] = None,
539
+ num_speakers: typing.Optional[int] = None,
540
+ callback: typing.Optional[BulkJobCallbackParams] = OMIT,
541
+ request_options: typing.Optional[RequestOptions] = None,
542
+ ) -> "AsyncSpeechToTextJob":
543
+ """
544
+ Create a new Speech-to-Text bulk job.
545
+
546
+ Parameters
547
+ ----------
548
+ model : SpeechToTextModel, default="saarika:v2.5"
549
+ The model to use for transcription.
550
+
551
+ with_diarization : typing.Optional[bool], default=False
552
+ Whether to enable speaker diarization (distinguishing who said what).
553
+
554
+ with_timestamps : typing.Optional[bool], default=False
555
+ Whether to include word-level timestamps in the transcription output.
556
+
557
+ language_code : typing.Optional[SpeechToTextLanguage], default=None
558
+ The language code of the input audio (e.g., "hi-IN", "bn-IN").
559
+
560
+ num_speakers : typing.Optional[int], default=None
561
+ The number of distinct speakers in the audio, if known.
562
+
563
+ callback : typing.Optional[BulkJobCallbackParams], default=OMIT
564
+ Optional callback configuration to receive job completion events.
565
+
566
+ request_options : typing.Optional[RequestOptions], default=None
567
+ Request-specific configuration.
568
+
569
+ Returns
570
+ -------
571
+ AsyncSpeechToTextJob
572
+ A handle to the newly created job.
573
+ """
574
+ response = await self.initialise(
575
+ job_parameters=SpeechToTextJobParametersParams(
576
+ language_code=language_code,
577
+ model=model,
578
+ with_diarization=with_diarization,
579
+ with_timestamps=with_timestamps,
580
+ num_speakers=num_speakers, # type: ignore[typeddict-item]
581
+ ),
582
+ callback=callback,
583
+ request_options=request_options,
584
+ )
585
+ return AsyncSpeechToTextJob(job_id=response.job_id, client=self)
586
+
587
+ async def get_job(self, job_id: str) -> "AsyncSpeechToTextJob":
588
+ """
589
+ Get an existing Speech-to-Text job handle by job ID.
590
+
591
+ Parameters
592
+ ----------
593
+ job_id : str
594
+ The job ID of the previously created speech-to-text job.
595
+
596
+ Returns
597
+ -------
598
+ AsyncSpeechToTextJob
599
+ A job handle which can be used to check status or retrieve results.
600
+ """
601
+ return AsyncSpeechToTextJob(job_id=job_id, client=self)
@@ -0,0 +1,484 @@
1
+ import asyncio
2
+ import mimetypes
3
+ import os
4
+ import time
5
+ import typing
6
+ import httpx
7
+
8
+ from ..types import JobStatusV1Response
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from .client import AsyncSpeechToTextJobClient, SpeechToTextJobClient
12
+
13
+
14
+ class AsyncSpeechToTextJob:
15
+ def __init__(self, job_id: str, client: "AsyncSpeechToTextJobClient"):
16
+ """
17
+ Initialize the asynchronous speech-to-text job.
18
+
19
+ Parameters
20
+ ----------
21
+ job_id : str
22
+ The unique job identifier returned from a previous job initialization.
23
+
24
+ client : AsyncSpeechToTextJobClient
25
+ The async client instance used to create the job.
26
+
27
+ !!! important
28
+ This must be the **same client instance** that was used to initialize
29
+ the job originally, as it contains the subscription key and configuration
30
+ required to authenticate and manage the job.
31
+
32
+ """
33
+ self._job_id = job_id
34
+ self._client = client
35
+
36
+ @property
37
+ def job_id(self) -> str:
38
+ """
39
+ Returns the job ID associated with this job instance.
40
+
41
+ Returns
42
+ -------
43
+ str
44
+ """
45
+ return self._job_id
46
+
47
+ async def upload_files(
48
+ self, file_paths: typing.Sequence[str], timeout: float = 60.0
49
+ ) -> bool:
50
+ """
51
+ Upload input audio files for the speech-to-text job.
52
+
53
+ Parameters
54
+ ----------
55
+ file_paths : Sequence[str]
56
+ List of full paths to local audio files.
57
+
58
+ timeout : float, optional
59
+ The maximum time to wait for the upload to complete (in seconds),
60
+ by default 60.0
61
+ Returns
62
+ -------
63
+ bool
64
+ True if all files are uploaded successfully.
65
+ """
66
+ upload_links = await self._client.get_upload_links(
67
+ job_id=self._job_id,
68
+ files=[os.path.basename(p) for p in file_paths],
69
+ )
70
+ client_timeout = httpx.Timeout(timeout=timeout)
71
+ async with httpx.AsyncClient(timeout=client_timeout) as session:
72
+ for path in file_paths:
73
+ file_name = os.path.basename(path)
74
+ url = upload_links.upload_urls[file_name].file_url
75
+ with open(path, "rb") as f:
76
+ content_type, _ = mimetypes.guess_type(path)
77
+ if content_type is None:
78
+ content_type = "audio/wav"
79
+ response = await session.put(
80
+ url,
81
+ content=f.read(),
82
+ headers={
83
+ "x-ms-blob-type": "BlockBlob",
84
+ "Content-Type": content_type,
85
+ },
86
+ )
87
+ if response.status_code != 201:
88
+ raise RuntimeError(
89
+ f"Upload failed for {file_name}: {response.status_code}"
90
+ )
91
+ return True
92
+
93
+ async def wait_until_complete(
94
+ self, poll_interval: int = 5, timeout: int = 600
95
+ ) -> JobStatusV1Response:
96
+ """
97
+ Polls job status until it completes or fails.
98
+
99
+ Parameters
100
+ ----------
101
+ poll_interval : int, optional
102
+ Time in seconds between polling attempts (default is 5).
103
+
104
+ timeout : int, optional
105
+ Maximum time to wait for completion in seconds (default is 600).
106
+
107
+ Returns
108
+ -------
109
+ JobStatusV1Response
110
+ Final job status.
111
+
112
+ Raises
113
+ ------
114
+ TimeoutError
115
+ If the job does not complete within the given timeout.
116
+ """
117
+ start = asyncio.get_event_loop().time()
118
+ while True:
119
+ status = await self.get_status()
120
+ state = status.job_state.lower()
121
+ if state in {"completed", "failed"}:
122
+ return status
123
+ if asyncio.get_event_loop().time() - start > timeout:
124
+ raise TimeoutError(
125
+ f"Job {self._job_id} did not complete within {timeout} seconds."
126
+ )
127
+ await asyncio.sleep(poll_interval)
128
+
129
+ async def get_output_mappings(self) -> typing.List[typing.Dict[str, str]]:
130
+ """
131
+ Get the mapping of input files to their corresponding output files.
132
+
133
+ Returns
134
+ -------
135
+ List[Dict[str, str]]
136
+ List of mappings with keys 'input_file' and 'output_file'.
137
+ """
138
+ job_status = await self.get_status()
139
+ return [
140
+ {
141
+ "input_file": detail.inputs[0].file_name,
142
+ "output_file": detail.outputs[0].file_name,
143
+ }
144
+ for detail in (job_status.job_details or [])
145
+ if detail.inputs and detail.outputs
146
+ ]
147
+
148
+ async def download_outputs(self, output_dir: str) -> bool:
149
+ """
150
+ Download output files to the specified directory.
151
+
152
+ Parameters
153
+ ----------
154
+ output_dir : str
155
+ Local directory where outputs will be saved.
156
+
157
+ Returns
158
+ -------
159
+ bool
160
+ True if all files downloaded successfully.
161
+
162
+ Raises
163
+ ------
164
+ RuntimeError
165
+ If a file fails to download.
166
+ """
167
+ mappings = await self.get_output_mappings()
168
+ file_names = [m["output_file"] for m in mappings]
169
+ download_links = await self._client.get_download_links(
170
+ job_id=self._job_id, files=file_names
171
+ )
172
+
173
+ os.makedirs(output_dir, exist_ok=True)
174
+ async with httpx.AsyncClient() as session:
175
+ for m in mappings:
176
+ url = download_links.download_urls[m["output_file"]].file_url
177
+ response = await session.get(url)
178
+ if response.status_code != 200:
179
+ raise RuntimeError(
180
+ f"Download failed for {m['output_file']}: {response.status_code}"
181
+ )
182
+ output_path = os.path.join(output_dir, f"{m['input_file']}.json")
183
+ with open(output_path, "wb") as f:
184
+ f.write(response.content)
185
+ return True
186
+
187
+ async def get_status(self) -> JobStatusV1Response:
188
+ """
189
+ Retrieve the current status of the job.
190
+
191
+ Returns
192
+ -------
193
+ JobStatusV1Response
194
+ """
195
+ return await self._client.get_status(self._job_id)
196
+
197
+ async def start(self) -> JobStatusV1Response:
198
+ """
199
+ Start the speech-to-text job processing.
200
+
201
+ Returns
202
+ -------
203
+ JobStatusV1Response
204
+ """
205
+ return await self._client.start(job_id=self._job_id)
206
+
207
+ async def exists(self) -> bool:
208
+ """
209
+ Check if the job exists in the system.
210
+
211
+ Returns
212
+ -------
213
+ bool
214
+ """
215
+ try:
216
+ await self.get_status()
217
+ return True
218
+ except httpx.HTTPStatusError:
219
+ return False
220
+
221
+ async def is_complete(self) -> bool:
222
+ """
223
+ Check if the job is either completed or failed.
224
+
225
+ Returns
226
+ -------
227
+ bool
228
+ """
229
+ state = (await self.get_status()).job_state.lower()
230
+ return state in {"completed", "failed"}
231
+
232
+ async def is_successful(self) -> bool:
233
+ """
234
+ Check if the job completed successfully.
235
+
236
+ Returns
237
+ -------
238
+ bool
239
+ """
240
+ return (await self.get_status()).job_state.lower() == "completed"
241
+
242
+ async def is_failed(self) -> bool:
243
+ """
244
+ Check if the job has failed.
245
+
246
+ Returns
247
+ -------
248
+ bool
249
+ """
250
+ return (await self.get_status()).job_state.lower() == "failed"
251
+
252
+
253
+ class SpeechToTextJob:
254
+ def __init__(self, job_id: str, client: "SpeechToTextJobClient"):
255
+ """
256
+ Initialize the synchronous speech-to-text job.
257
+
258
+ Parameters
259
+ ----------
260
+ job_id : str
261
+ The unique job identifier returned from a previous job initialization.
262
+
263
+ client : SpeechToTextJobClient
264
+ The client instance used to create the job.
265
+
266
+ !!! important
267
+ This must be the **same client instance** that was used to initialize
268
+ the job originally, as it contains the subscription key and configuration
269
+ required to authenticate and manage the job.
270
+
271
+ """
272
+ self._job_id = job_id
273
+ self._client = client
274
+
275
+ @property
276
+ def job_id(self) -> str:
277
+ """
278
+ Returns the job ID associated with this job instance.
279
+
280
+ Returns
281
+ -------
282
+ str
283
+ """
284
+ return self._job_id
285
+
286
+ def upload_files(
287
+ self, file_paths: typing.Sequence[str], timeout: float = 60.0
288
+ ) -> bool:
289
+ """
290
+ Upload input audio files for the speech-to-text job.
291
+
292
+ Parameters
293
+ ----------
294
+ file_paths : Sequence[str]
295
+ List of full paths to local audio files.
296
+
297
+ timeout : float, optional
298
+ The maximum time to wait for the upload to complete (in seconds),
299
+ by default 60.0
300
+ Returns
301
+ -------
302
+ bool
303
+ True if all files are uploaded successfully.
304
+ """
305
+ upload_links = self._client.get_upload_links(
306
+ job_id=self._job_id, files=[os.path.basename(p) for p in file_paths]
307
+ )
308
+ client_timeout = httpx.Timeout(timeout=timeout)
309
+ with httpx.Client(timeout=client_timeout) as client:
310
+ for path in file_paths:
311
+ file_name = os.path.basename(path)
312
+ url = upload_links.upload_urls[file_name].file_url
313
+ with open(path, "rb") as f:
314
+ response = client.put(
315
+ url,
316
+ content=f,
317
+ headers={
318
+ "x-ms-blob-type": "BlockBlob",
319
+ "Content-Type": "audio/wav",
320
+ },
321
+ )
322
+ if response.status_code != 201:
323
+ raise RuntimeError(
324
+ f"Upload failed for {file_name}: {response.status_code}"
325
+ )
326
+ return True
327
+
328
+ def wait_until_complete(
329
+ self, poll_interval: int = 5, timeout: int = 600
330
+ ) -> JobStatusV1Response:
331
+ """
332
+ Polls job status until it completes or fails.
333
+
334
+ Parameters
335
+ ----------
336
+ poll_interval : int, optional
337
+ Time in seconds between polling attempts (default is 5).
338
+
339
+ timeout : int, optional
340
+ Maximum time to wait for completion in seconds (default is 600).
341
+
342
+ Returns
343
+ -------
344
+ JobStatusV1Response
345
+ Final job status.
346
+
347
+ Raises
348
+ ------
349
+ TimeoutError
350
+ If the job does not complete within the given timeout.
351
+ """
352
+ start = time.monotonic()
353
+ while True:
354
+ status = self.get_status()
355
+ state = status.job_state.lower()
356
+ if state in {"completed", "failed"}:
357
+ return status
358
+ if time.monotonic() - start > timeout:
359
+ raise TimeoutError(
360
+ f"Job {self._job_id} did not complete within {timeout} seconds."
361
+ )
362
+ time.sleep(poll_interval)
363
+
364
+ def get_output_mappings(self) -> typing.List[typing.Dict[str, str]]:
365
+ """
366
+ Get the mapping of input files to their corresponding output files.
367
+
368
+ Returns
369
+ -------
370
+ List[Dict[str, str]]
371
+ List of mappings with keys 'input_file' and 'output_file'.
372
+ """
373
+ job_status = self.get_status()
374
+ return [
375
+ {
376
+ "input_file": detail.inputs[0].file_name,
377
+ "output_file": detail.outputs[0].file_name,
378
+ }
379
+ for detail in (job_status.job_details or [])
380
+ if detail.inputs and detail.outputs
381
+ ]
382
+
383
+ def download_outputs(self, output_dir: str) -> bool:
384
+ """
385
+ Download output files to the specified directory.
386
+
387
+ Parameters
388
+ ----------
389
+ output_dir : str
390
+ Local directory where outputs will be saved.
391
+
392
+ Returns
393
+ -------
394
+ bool
395
+ True if all files downloaded successfully.
396
+
397
+ Raises
398
+ ------
399
+ RuntimeError
400
+ If a file fails to download.
401
+ """
402
+ mappings = self.get_output_mappings()
403
+ file_names = [m["output_file"] for m in mappings]
404
+ download_links = self._client.get_download_links(
405
+ job_id=self._job_id, files=file_names
406
+ )
407
+
408
+ os.makedirs(output_dir, exist_ok=True)
409
+ with httpx.Client() as client:
410
+ for m in mappings:
411
+ url = download_links.download_urls[m["output_file"]].file_url
412
+ response = client.get(url)
413
+ if response.status_code != 200:
414
+ raise RuntimeError(
415
+ f"Download failed for {m['output_file']}: {response.status_code}"
416
+ )
417
+ output_path = os.path.join(output_dir, f"{m['input_file']}.json")
418
+ with open(output_path, "wb") as f:
419
+ f.write(response.content)
420
+ return True
421
+
422
+ def get_status(self) -> JobStatusV1Response:
423
+ """
424
+ Retrieve the current status of the job.
425
+
426
+ Returns
427
+ -------
428
+ JobStatusV1Response
429
+ """
430
+ return self._client.get_status(self._job_id)
431
+
432
+ def start(self) -> JobStatusV1Response:
433
+ """
434
+ Start the speech-to-text job processing.
435
+
436
+ Returns
437
+ -------
438
+ JobStatusV1Response
439
+ """
440
+ return self._client.start(job_id=self._job_id)
441
+
442
+ def exists(self) -> bool:
443
+ """
444
+ Check if the job exists in the system.
445
+
446
+ Returns
447
+ -------
448
+ bool
449
+ """
450
+ try:
451
+ self.get_status()
452
+ return True
453
+ except httpx.HTTPStatusError:
454
+ return False
455
+
456
+ def is_complete(self) -> bool:
457
+ """
458
+ Check if the job is either completed or failed.
459
+
460
+ Returns
461
+ -------
462
+ bool
463
+ """
464
+ return self.get_status().job_state.lower() in {"completed", "failed"}
465
+
466
+ def is_successful(self) -> bool:
467
+ """
468
+ Check if the job completed successfully.
469
+
470
+ Returns
471
+ -------
472
+ bool
473
+ """
474
+ return self.get_status().job_state.lower() == "completed"
475
+
476
+ def is_failed(self) -> bool:
477
+ """
478
+ Check if the job has failed.
479
+
480
+ Returns
481
+ -------
482
+ bool
483
+ """
484
+ 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)
@@ -0,0 +1,492 @@
1
+ import asyncio
2
+ import mimetypes
3
+ import os
4
+ import time
5
+ import typing
6
+ import httpx
7
+
8
+ from ..types import JobStatusV1Response
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from .client import (
12
+ AsyncSpeechToTextTranslateJobClient,
13
+ SpeechToTextTranslateJobClient,
14
+ )
15
+
16
+
17
+ class AsyncSpeechToTextTranslateJob:
18
+ def __init__(
19
+ self,
20
+ job_id: str,
21
+ client: "AsyncSpeechToTextTranslateJobClient",
22
+ ):
23
+ """
24
+ Initialise the asynchronous speech-to-text-translate-translate job.
25
+
26
+ Parameters
27
+ ----------
28
+ job_id : str
29
+ The unique job identifier returned from a previous job initialisation.
30
+
31
+ client : AsyncSpeechToTextTranslateJobClient
32
+ The async client instance used to create the job.
33
+
34
+ !!! important
35
+ This must be the **same client instance** that was used to initialise
36
+ the job originally, as it contains the subscription key and configuration
37
+ required to authenticate and manage the job.
38
+
39
+ """
40
+ self._job_id = job_id
41
+ self._client = client
42
+
43
+ @property
44
+ def job_id(self) -> str:
45
+ """
46
+ Returns the job ID associated with this job instance.
47
+
48
+ Returns
49
+ -------
50
+ str
51
+ """
52
+ return self._job_id
53
+
54
+ async def upload_files(
55
+ self, file_paths: typing.Sequence[str], timeout: float = 60.0
56
+ ) -> bool:
57
+ """
58
+ Upload input audio files for the speech-to-text-translate job.
59
+
60
+ Parameters
61
+ ----------
62
+ file_paths : Sequence[str]
63
+ List of full paths to local audio files.
64
+
65
+ timeout : float, optional
66
+ The maximum time to wait for the upload to complete (in seconds),
67
+ by default 60.0
68
+ Returns
69
+ -------
70
+ bool
71
+ True if all files are uploaded successfully.
72
+ """
73
+ upload_links = await self._client.get_upload_links(
74
+ job_id=self._job_id,
75
+ files=[os.path.basename(p) for p in file_paths],
76
+ )
77
+ client_timeout = httpx.Timeout(timeout=timeout)
78
+ async with httpx.AsyncClient(timeout=client_timeout) as session:
79
+ for path in file_paths:
80
+ file_name = os.path.basename(path)
81
+ url = upload_links.upload_urls[file_name].file_url
82
+ with open(path, "rb") as f:
83
+ response = await session.put(
84
+ url,
85
+ content=f.read(),
86
+ headers={
87
+ "x-ms-blob-type": "BlockBlob",
88
+ "Content-Type": "audio/wav",
89
+ },
90
+ )
91
+ if response.status_code != 201:
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 response.status_code != 200:
183
+ raise RuntimeError(
184
+ f"Download failed for {m['output_file']}: {response.status_code}"
185
+ )
186
+ output_path = os.path.join(output_dir, f"{m['input_file']}.json")
187
+ with open(output_path, "wb") as f:
188
+ f.write(response.content)
189
+ return True
190
+
191
+ async def get_status(self) -> JobStatusV1Response:
192
+ """
193
+ Retrieve the current status of the job.
194
+
195
+ Returns
196
+ -------
197
+ JobStatusV1Response
198
+ """
199
+ return await self._client.get_status(self._job_id)
200
+
201
+ async def start(self) -> JobStatusV1Response:
202
+ """
203
+ Start the speech-to-text-translate job processing.
204
+
205
+ Returns
206
+ -------
207
+ JobStatusV1Response
208
+ """
209
+ return await self._client.start(job_id=self._job_id)
210
+
211
+ async def exists(self) -> bool:
212
+ """
213
+ Check if the job exists in the system.
214
+
215
+ Returns
216
+ -------
217
+ bool
218
+ """
219
+ try:
220
+ await self.get_status()
221
+ return True
222
+ except httpx.HTTPStatusError:
223
+ return False
224
+
225
+ async def is_complete(self) -> bool:
226
+ """
227
+ Check if the job is either completed or failed.
228
+
229
+ Returns
230
+ -------
231
+ bool
232
+ """
233
+ state = (await self.get_status()).job_state.lower()
234
+ return state in {"completed", "failed"}
235
+
236
+ async def is_successful(self) -> bool:
237
+ """
238
+ Check if the job completed successfully.
239
+
240
+ Returns
241
+ -------
242
+ bool
243
+ """
244
+ return (await self.get_status()).job_state.lower() == "completed"
245
+
246
+ async def is_failed(self) -> bool:
247
+ """
248
+ Check if the job has failed.
249
+
250
+ Returns
251
+ -------
252
+ bool
253
+ """
254
+ return (await self.get_status()).job_state.lower() == "failed"
255
+
256
+
257
+ class SpeechToTextTranslateJob:
258
+ def __init__(self, job_id: str, client: "SpeechToTextTranslateJobClient"):
259
+ """
260
+ Initialise the synchronous speech-to-text-translate job.
261
+
262
+ Parameters
263
+ ----------
264
+ job_id : str
265
+ The unique job identifier returned from a previous job initialisation.
266
+
267
+ client : SpeechToTextTranslateJobClient
268
+ The client instance used to create the job.
269
+
270
+ !!! important
271
+ This must be the **same client instance** that was used to initialise
272
+ the job originally, as it contains the subscription key and configuration
273
+ required to authenticate and manage the job.
274
+
275
+ """
276
+ self._job_id = job_id
277
+ self._client = client
278
+
279
+ @property
280
+ def job_id(self) -> str:
281
+ """
282
+ Returns the job ID associated with this job instance.
283
+
284
+ Returns
285
+ -------
286
+ str
287
+ """
288
+ return self._job_id
289
+
290
+ def upload_files(
291
+ self, file_paths: typing.Sequence[str], timeout: float = 60.0
292
+ ) -> bool:
293
+ """
294
+ Upload input audio files for the speech-to-text-translate job.
295
+
296
+ Parameters
297
+ ----------
298
+ file_paths : Sequence[str]
299
+ List of full paths to local audio files.
300
+
301
+ timeout : float, optional
302
+ The maximum time to wait for the upload to complete (in seconds),
303
+ by default 60.0
304
+
305
+ Returns
306
+ -------
307
+ bool
308
+ True if all files are uploaded successfully.
309
+ """
310
+ upload_links = self._client.get_upload_links(
311
+ job_id=self._job_id, files=[os.path.basename(p) for p in file_paths]
312
+ )
313
+ client_timeout = httpx.Timeout(timeout=timeout)
314
+ with httpx.Client(timeout=client_timeout) as client:
315
+ for path in file_paths:
316
+ file_name = os.path.basename(path)
317
+ url = upload_links.upload_urls[file_name].file_url
318
+ content_type, _ = mimetypes.guess_type(path)
319
+ if content_type is None:
320
+ content_type = "audio/wav"
321
+ with open(path, "rb") as f:
322
+ response = client.put(
323
+ url,
324
+ content=f,
325
+ headers={
326
+ "x-ms-blob-type": "BlockBlob",
327
+ "Content-Type": content_type,
328
+ },
329
+ )
330
+ if response.status_code != 201:
331
+ raise RuntimeError(
332
+ f"Upload failed for {file_name}: {response.status_code}"
333
+ )
334
+ return True
335
+
336
+ def wait_until_complete(
337
+ self, poll_interval: int = 5, timeout: int = 600
338
+ ) -> JobStatusV1Response:
339
+ """
340
+ Polls job status until it completes or fails.
341
+
342
+ Parameters
343
+ ----------
344
+ poll_interval : int, optional
345
+ Time in seconds between polling attempts (default is 5).
346
+
347
+ timeout : int, optional
348
+ Maximum time to wait for completion in seconds (default is 600).
349
+
350
+ Returns
351
+ -------
352
+ JobStatusV1Response
353
+ Final job status.
354
+
355
+ Raises
356
+ ------
357
+ TimeoutError
358
+ If the job does not complete within the given timeout.
359
+ """
360
+ start = time.monotonic()
361
+ while True:
362
+ status = self.get_status()
363
+ state = status.job_state.lower()
364
+ if state in {"completed", "failed"}:
365
+ return status
366
+ if time.monotonic() - start > timeout:
367
+ raise TimeoutError(
368
+ f"Job {self._job_id} did not complete within {timeout} seconds."
369
+ )
370
+ time.sleep(poll_interval)
371
+
372
+ def get_output_mappings(self) -> typing.List[typing.Dict[str, str]]:
373
+ """
374
+ Get the mapping of input files to their corresponding output files.
375
+
376
+ Returns
377
+ -------
378
+ List[Dict[str, str]]
379
+ List of mappings with keys 'input_file' and 'output_file'.
380
+ """
381
+ job_status = self.get_status()
382
+ return [
383
+ {
384
+ "input_file": detail.inputs[0].file_name,
385
+ "output_file": detail.outputs[0].file_name,
386
+ }
387
+ for detail in (job_status.job_details or [])
388
+ if detail.inputs and detail.outputs
389
+ ]
390
+
391
+ def download_outputs(self, output_dir: str) -> bool:
392
+ """
393
+ Download output files to the specified directory.
394
+
395
+ Parameters
396
+ ----------
397
+ output_dir : str
398
+ Local directory where outputs will be saved.
399
+
400
+ Returns
401
+ -------
402
+ bool
403
+ True if all files downloaded successfully.
404
+
405
+ Raises
406
+ ------
407
+ RuntimeError
408
+ If a file fails to download.
409
+ """
410
+ mappings = self.get_output_mappings()
411
+ file_names = [m["output_file"] for m in mappings]
412
+ download_links = self._client.get_download_links(
413
+ job_id=self._job_id, files=file_names
414
+ )
415
+
416
+ os.makedirs(output_dir, exist_ok=True)
417
+ with httpx.Client() as client:
418
+ for m in mappings:
419
+ url = download_links.download_urls[m["output_file"]].file_url
420
+ response = client.get(url)
421
+ if response.status_code != 200:
422
+ raise RuntimeError(
423
+ f"Download failed for {m['output_file']}: {response.status_code}"
424
+ )
425
+ output_path = os.path.join(output_dir, f"{m['input_file']}.json")
426
+ with open(output_path, "wb") as f:
427
+ f.write(response.content)
428
+ return True
429
+
430
+ def get_status(self) -> JobStatusV1Response:
431
+ """
432
+ Retrieve the current status of the job.
433
+
434
+ Returns
435
+ -------
436
+ JobStatusV1Response
437
+ """
438
+ return self._client.get_status(self._job_id)
439
+
440
+ def start(self) -> JobStatusV1Response:
441
+ """
442
+ Start the speech-to-text-translate job processing.
443
+
444
+ Returns
445
+ -------
446
+ JobStatusV1Response
447
+ """
448
+ return self._client.start(job_id=self._job_id)
449
+
450
+ def exists(self) -> bool:
451
+ """
452
+ Check if the job exists in the system.
453
+
454
+ Returns
455
+ -------
456
+ bool
457
+ """
458
+ try:
459
+ self.get_status()
460
+ return True
461
+ except httpx.HTTPStatusError:
462
+ return False
463
+
464
+ def is_complete(self) -> bool:
465
+ """
466
+ Check if the job is either completed or failed.
467
+
468
+ Returns
469
+ -------
470
+ bool
471
+ """
472
+ return self.get_status().job_state.lower() in {"completed", "failed"}
473
+
474
+ def is_successful(self) -> bool:
475
+ """
476
+ Check if the job completed successfully.
477
+
478
+ Returns
479
+ -------
480
+ bool
481
+ """
482
+ return self.get_status().job_state.lower() == "completed"
483
+
484
+ def is_failed(self) -> bool:
485
+ """
486
+ Check if the job has failed.
487
+
488
+ Returns
489
+ -------
490
+ bool
491
+ """
492
+ return self.get_status().job_state.lower() == "failed"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sarvamai
3
- Version: 0.1.13a2
3
+ Version: 0.1.14
4
4
  Summary:
5
5
  Requires-Python: >=3.8,<4.0
6
6
  Classifier: Intended Audience :: Developers
@@ -5,7 +5,7 @@ sarvamai/chat/raw_client.py,sha256=A2kRuZcVWlJhyYCD7YKgqNkZEp3cYa1731KhRkhirU0,1
5
5
  sarvamai/client.py,sha256=J30X_os1lPf8Wml0KDFEf6p8VGHhgF_lf3nw1T2D3qo,8207
6
6
  sarvamai/core/__init__.py,sha256=YE2CtXeASe1RAbaI39twKWYKCuT4tW5is9HWHhJjR_g,1653
7
7
  sarvamai/core/api_error.py,sha256=44vPoTyWN59gonCIZMdzw7M1uspygiLnr3GNFOoVL2Q,614
8
- sarvamai/core/client_wrapper.py,sha256=FC_3Kep5NGzWOSiwBhHJ1vKfR3_Pqm6CZ7kWWh_r5AI,2570
8
+ sarvamai/core/client_wrapper.py,sha256=tGjCU2En32hYgjFeTGET-nMT2b4f0S_moWlvMwdIPO4,2566
9
9
  sarvamai/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
10
10
  sarvamai/core/events.py,sha256=HvKBdSoYcFetk7cgNXb7FxuY-FtY8NtUhZIN7mGVx8U,1159
11
11
  sarvamai/core/file.py,sha256=d4NNbX8XvXP32z8KpK2Xovv33nFfruIrpz0QWxlgpZk,2663
@@ -87,7 +87,8 @@ sarvamai/speech_to_text/__init__.py,sha256=_VhToAyIt_5axN6CLJwtxg3-CO7THa_23pbUz
87
87
  sarvamai/speech_to_text/client.py,sha256=lp2G2fI9SUbeOBBE1S5tjcp-Xb8wIhAuVadLKwXveh8,11003
88
88
  sarvamai/speech_to_text/raw_client.py,sha256=A_56vEVeJdyttVJRiFxTMJ4n-s4l_PS8rI1DiLZlOmc,25331
89
89
  sarvamai/speech_to_text_job/__init__.py,sha256=_VhToAyIt_5axN6CLJwtxg3-CO7THa_23pbUzqhXJa4,85
90
- sarvamai/speech_to_text_job/client.py,sha256=MeX6V_34pbeWczBdsiT3VmAp8O4e4QmNwXNR1ue6s7A,12577
90
+ sarvamai/speech_to_text_job/client.py,sha256=WSGBJxYcNxl77Zd1X6VVWjg4zshqecXf6WCyhfLXVlI,18007
91
+ sarvamai/speech_to_text_job/job.py,sha256=xeriu0dMQ-hCu9fxGoCPAmNalUSjmXCH_kypROI4ifY,14537
91
92
  sarvamai/speech_to_text_job/raw_client.py,sha256=OZTPzMhAn-ckE_xKzfZ9QLsEX5EZVOJS0Pf-PBa19jM,48200
92
93
  sarvamai/speech_to_text_streaming/__init__.py,sha256=q7QygMmZCHJ-4FMhhL_6XNV_dsqlIFRCO1iSxoyxaaY,437
93
94
  sarvamai/speech_to_text_streaming/client.py,sha256=AzStfZDXhu2YAJEpnVbsy0WObub5ctlGBzqfeYOUlpA,8442
@@ -99,7 +100,8 @@ sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_language_code.p
99
100
  sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_model.py,sha256=b6F4ymgz4got6KVDqrweYvkET8itze63wUwWyjqDlO4,180
100
101
  sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_vad_signals.py,sha256=8wiFOB7WDMbYCcMTYgNFJaIjEytYeXpJLwr_O_mH0TI,172
101
102
  sarvamai/speech_to_text_translate_job/__init__.py,sha256=_VhToAyIt_5axN6CLJwtxg3-CO7THa_23pbUzqhXJa4,85
102
- sarvamai/speech_to_text_translate_job/client.py,sha256=LxmP_T_KsPtI4Xg25RqAVN0Nn1c0V1VLZecZzngiCJ0,13674
103
+ sarvamai/speech_to_text_translate_job/client.py,sha256=xu8kYtCESDB7LzL8YKBUq5qhTPMIl3_H3XD2L_7y4UU,18969
104
+ sarvamai/speech_to_text_translate_job/job.py,sha256=Yc2bdWQVUZdJTL8AbY11-WsdaNN8h8iJN-Nsy_pa-4Y,14724
103
105
  sarvamai/speech_to_text_translate_job/raw_client.py,sha256=dAitbu2B9afPK6iT9zNjUJnE5BIr5-lrAlwrfwFxdkU,49507
104
106
  sarvamai/speech_to_text_translate_streaming/__init__.py,sha256=_hmlce1Zs1grylysZhBUdtKfkaUROwVydtwz6l-1qqg,411
105
107
  sarvamai/speech_to_text_translate_streaming/client.py,sha256=xPPg5_JgpH8tYDUte6FGtpzXO2LGBUSRADN-ICqqA6U,8286
@@ -207,6 +209,6 @@ sarvamai/types/transliterate_mode.py,sha256=1jSEMlGcoLkWuk12TgoOpSgwifa4rThGKZ1h
207
209
  sarvamai/types/transliterate_source_language.py,sha256=bSY9wJszF0sg-Cgg6F-YcWC8ly1mIlj9rqa15-jBtx8,283
208
210
  sarvamai/types/transliteration_response.py,sha256=yt-lzTbDeJ_ZL4I8kQa6oESxA9ebeJJY7LfFHpdEsmM,815
209
211
  sarvamai/version.py,sha256=Qkp3Ee9YH-O9RTix90e0i7iNrFAGN-QDt2AFwGA4n8k,75
210
- sarvamai-0.1.13a2.dist-info/METADATA,sha256=-i6GJmDknM_EKjKfdo2vWiwx0ipLvO5tmR6K8Sh9soY,26753
211
- sarvamai-0.1.13a2.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
212
- sarvamai-0.1.13a2.dist-info/RECORD,,
212
+ sarvamai-0.1.14.dist-info/METADATA,sha256=sNKG90iusG5kajxY45Mj8OhOq1MpnPzf69-E9MWtCIU,26751
213
+ sarvamai-0.1.14.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
214
+ sarvamai-0.1.14.dist-info/RECORD,,