Metadata-Version: 2.4
Name: shunyalabsai
Version: 1.0.0
Summary: Shunyalabs Python SDK — ASR, TTS, and Conversational AI
Author-email: Shunyalabs <support@shunyalabs.ai>
License: MIT
Project-URL: homepage, https://github.com/Shunyalabsai/shunyalabs-python-sdk
Project-URL: documentation, https://docs.shunyalabs.ai/
Project-URL: repository, https://github.com/Shunyalabsai/shunyalabs-python-sdk
Project-URL: issues, https://github.com/Shunyalabsai/shunyalabs-python-sdk/issues
Keywords: shunyalabs,speech-to-text,text-to-speech,asr,tts,transcription,conversational-ai,real-time,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic>=2.0
Provides-Extra: asr
Requires-Dist: aiohttp>=3.8; extra == "asr"
Requires-Dist: httpx>=0.27; extra == "asr"
Requires-Dist: aiofiles>=22.0; extra == "asr"
Requires-Dist: websockets>=10.0; extra == "asr"
Provides-Extra: tts
Requires-Dist: aiohttp>=3.8; extra == "tts"
Requires-Dist: httpx>=0.27; extra == "tts"
Requires-Dist: websockets>=10.0; extra == "tts"
Provides-Extra: flow
Requires-Dist: websockets>=10.0; extra == "flow"
Provides-Extra: all
Requires-Dist: aiohttp>=3.8; extra == "all"
Requires-Dist: httpx>=0.27; extra == "all"
Requires-Dist: aiofiles>=22.0; extra == "all"
Requires-Dist: websockets>=10.0; extra == "all"
Provides-Extra: extras
Requires-Dist: sounddevice>=0.4; extra == "extras"
Provides-Extra: dev
Requires-Dist: aiohttp>=3.8; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: aiofiles>=22.0; extra == "dev"
Requires-Dist: websockets>=10.0; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-aiofiles; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest-mock; extra == "dev"
Requires-Dist: build; extra == "dev"
Dynamic: license-file

# Shunyalabs Python SDK

[![PyPI](https://img.shields.io/pypi/v/shunyalabsai.svg)](https://pypi.org/project/shunyalabsai/)
[![Python](https://img.shields.io/pypi/pyversions/shunyalabsai.svg)](https://pypi.org/project/shunyalabsai/)
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)

The official Python SDK for [Shunyalabs](https://shunyalabs.ai) Speech AI APIs — **ASR** (speech-to-text) and **TTS** (text-to-speech).

Supports HTTP batch and WebSocket streaming modes with a fully async client.

## Installation

```bash
pip install shunyalabsai[all]
```

Install only what you need:

```bash
pip install shunyalabsai[ASR]     # Speech-to-text only
pip install shunyalabsai[TTS]     # Text-to-speech only
pip install shunyalabsai[extras]  # Audio playback helpers (sounddevice)
```

## Authentication

Authenticate with your API key. The SDK exchanges your API key for a short-lived access token automatically and refreshes it for you, so you never manage tokens yourself — just pass your key.

```python
from shunyalabs import AsyncShunyaClient

client = AsyncShunyaClient(api_key="your-api-key")
```

Or set the `SHUNYALABS_API_KEY` environment variable and omit `api_key=`.

---

## Quick Start

### TTS — Batch (HTTP)

```python
import asyncio
from shunyalabs import AsyncShunyaClient
from shunyalabs.tts import TTSConfig

async def main():
    async with AsyncShunyaClient(api_key="your-api-key") as client:
        result = await client.tts.synthesize(
            "Hello, world!",
            config=TTSConfig(model="zero-indic", voice="Varun", language="en"),
        )
        result.save("output.wav")
        print(f"{len(result.audio_data)} bytes saved")

asyncio.run(main())
```

### TTS — Streaming (WebSocket)

```python
import asyncio
from shunyalabs import AsyncShunyaClient
from shunyalabs.tts import TTSConfig

async def main():
    async with AsyncShunyaClient(api_key="your-api-key") as client:
        chunks = []
        async for audio in await client.tts.stream(
            "Hello, world!",
            config=TTSConfig(model="zero-indic", voice="Varun"),
        ):
            chunks.append(audio)
        print(f"{len(chunks)} chunks, {sum(len(c) for c in chunks)} bytes")

asyncio.run(main())
```

### ASR — Batch (HTTP)

```python
import asyncio
from shunyalabs import AsyncShunyaClient
from shunyalabs.asr import TranscriptionConfig

async def main():
    async with AsyncShunyaClient(api_key="your-api-key") as client:
        result = await client.asr.transcribe(
            "audio.wav",
            config=TranscriptionConfig(model="zero-indic"),
        )
        print(result.text)

asyncio.run(main())
```

### ASR — Streaming (WebSocket)

```python
import asyncio, subprocess
from shunyalabs import AsyncShunyaClient
from shunyalabs.asr import StreamingConfig, StreamingMessageType

async def main():
    async with AsyncShunyaClient(api_key="your-api-key") as client:
        conn = await client.asr.stream(
            config=StreamingConfig(language="en", sample_rate=16000),
        )

        @conn.on(StreamingMessageType.FINAL_SEGMENT)
        def on_seg(msg):
            print(f"[seg] {msg.text}")

        @conn.on(StreamingMessageType.FINAL)
        def on_final(msg):
            print(f"[final] {msg.text}")

        # Convert audio to 16kHz mono PCM and stream
        pcm = subprocess.run(
            ["ffmpeg", "-i", "audio.wav", "-ar", "16000", "-ac", "1", "-f", "s16le", "-"],
            capture_output=True,
        ).stdout

        for i in range(0, len(pcm), 4096):
            await conn.send_audio(pcm[i : i + 4096])

        await conn.end()
        await conn.close()

asyncio.run(main())
```

---

## API Reference

### Client Configuration

#### `AsyncShunyaClient`

| Parameter     | Type    | Default                      | Description                                           |
| ------------- | ------- | ---------------------------- | ----------------------------------------------------- |
| `api_key`     | `str`   | `None`                       | API key. Falls back to `SHUNYALABS_API_KEY` env var.  |
| `timeout`     | `float` | `60.0`                       | Request timeout in seconds.                           |
| `max_retries` | `int`   | `2`                          | Retries for failed requests (5xx, connection errors). |
| `asr_url`     | `str`   | `https://asrv2prod.shunyalabs.ai`           | ASR batch API base URL.                               |
| `asr_ws_url`  | `str`   | `wss://asrv2prod.shunyalabs.ai/v1/realtime` | ASR streaming WebSocket URL.                          |
| `tts_url`     | `str`   | `https://ttsv2.shunyalabs.ai`               | TTS batch API base URL.                               |
| `tts_ws_url`  | `str`   | `wss://ttsv2.shunyalabs.ai/v1/realtime`     | TTS streaming WebSocket URL.                          |

All URL parameters can also be set via environment variables: `SHUNYALABS_ASR_URL`, `SHUNYALABS_ASR_WS_URL`, `SHUNYALABS_TTS_URL`, `SHUNYALABS_TTS_WS_URL`.

Endpoints resolve by precedence: **explicit argument → endpoint returned by the token service → environment variable → built-in default.** So an endpoint can be moved centrally (delivered in the token response) with no SDK change or upgrade, an operator can override it with an environment variable, and code can pin it explicitly.

**Examples:**

```python
# Default — uses production endpoints
client = AsyncShunyaClient(api_key="your-api-key")

# Custom timeout and retries
client = AsyncShunyaClient(api_key="your-api-key", timeout=120.0, max_retries=5)

# Self-hosted endpoints
client = AsyncShunyaClient(
    api_key="your-api-key",
    asr_url="https://my-asr-server.example.com",
    tts_url="https://my-tts-server.example.com",
    tts_ws_url="wss://my-tts-server.example.com/v1/realtime",
)
```

---

### TTS API

#### `TTSConfig`

Configuration for synthesis requests. Passed as `config=` to `synthesize()` and `stream()`.

| Parameter              | Type           | Default      | Description                                                        |
| ---------------------- | -------------- | ------------ | ------------------------------------------------------------------ |
| `model`                | `str`          | `"zero-indic"` | Model name.                                                      |
| `language`             | `str`          | **required** | ISO 639 language code (e.g. `"en"`, `"hi"`, `"ta"`).               |
| `voice`                | `str`          | **required** | Speaker voice name (required unless `reference_wav` is given). See [Available Speakers](#available-speakers). |
| `response_format`      | `OutputFormat` | `"wav"`      | Output audio format. See [Output Formats](#output-formats).        |
| `speed`                | `float`        | `1.0`        | Speaking speed multiplier (0.25–4.0).                              |
| `trim_silence`         | `bool`         | `False`      | Trim leading/trailing silence from audio.                          |
| `background_audio`     | `str`          | `None`       | Preset name or base64-encoded background audio.                    |
| `background_volume`    | `float`        | `0.1`        | Background volume relative to speech (0.0–1.0).                    |

#### TTS Parameter Examples

**`model` — Select the TTS model**

```python
# Currently available: "zero-indic"
config = TTSConfig(model="zero-indic", voice="Rajesh")
result = await client.tts.synthesize("Hello!", config=config)
# Output: 48000 bytes saved to output.mp3
```

**`voice` — Choose a speaker**

```python
# Male English speaker
config = TTSConfig(model="zero-indic", voice="Varun")

# Female Hindi speaker
config = TTSConfig(model="zero-indic", voice="Sunita")

# Any speaker can speak any language — voice only controls vocal characteristics
config = TTSConfig(model="zero-indic", voice="Murugan")  # Tamil-native male speaking English
result = await client.tts.synthesize("Good morning, how are you?", config=config)
```

**`response_format` — Output audio format**

Values: `"pcm"`, `"wav"`, `"mp3"`, `"ogg_opus"`, `"flac"`, `"mulaw"`, `"alaw"`

```python
# MP3 — compressed, good for storage
config = TTSConfig(model="zero-indic", voice="Varun", response_format="mp3")
result = await client.tts.synthesize("Hello!", config=config)
result.save("output.mp3")
# Output: 12480 bytes (compressed)

# WAV — uncompressed, good for processing
config = TTSConfig(model="zero-indic", voice="Varun", response_format="wav")
result = await client.tts.synthesize("Hello!", config=config)
result.save("output.wav")
# Output: 96044 bytes (uncompressed with header)

# PCM — raw samples, for real-time pipelines
config = TTSConfig(model="zero-indic", voice="Varun", response_format="pcm")
result = await client.tts.synthesize("Hello!", config=config)
# Output: 96000 bytes (raw 16-bit samples)

# OGG Opus — compressed, good for web streaming
config = TTSConfig(model="zero-indic", voice="Varun", response_format="ogg_opus")

# mu-law / A-law — for telephony systems
config = TTSConfig(model="zero-indic", voice="Varun", response_format="mulaw")
config = TTSConfig(model="zero-indic", voice="Varun", response_format="alaw")
```

**`speed` — Speaking speed multiplier**

Range: `0.25` (very slow) to `4.0` (very fast). Default: `1.0`.

```python
# Slow — good for language learning
config = TTSConfig(model="zero-indic", voice="Nisha", speed=0.75)
result = await client.tts.synthesize("Take your time to understand this.", config=config)
# Output: longer audio, ~33% slower than normal

# Normal speed (default)
config = TTSConfig(model="zero-indic", voice="Nisha", speed=1.0)

# Fast — good for notifications or summaries
config = TTSConfig(model="zero-indic", voice="Nisha", speed=1.5)
result = await client.tts.synthesize("Quick update: your order has shipped.", config=config)
# Output: shorter audio, ~50% faster than normal

# Very fast
config = TTSConfig(model="zero-indic", voice="Nisha", speed=2.0)
```

**`trim_silence` — Remove silence padding**

```python
# Without trim (default) — audio may have leading/trailing silence
config = TTSConfig(model="zero-indic", voice="Rajesh", trim_silence=False)
result = await client.tts.synthesize("Hello.", config=config)
# Output: 64000 bytes (includes silence padding)

# With trim — tighter audio, no dead air
config = TTSConfig(model="zero-indic", voice="Rajesh", trim_silence=True)
result = await client.tts.synthesize("Hello.", config=config)
# Output: 48000 bytes (silence stripped)
```

**`background_audio` + `background_volume` — Add background music**

```python
import base64

# Using a preset name
config = TTSConfig(
    model="zero-indic",
    voice="Nisha",
    background_audio="cafe-ambience",
    background_volume=0.15,  # 15% volume relative to speech
)
result = await client.tts.synthesize("Welcome to our podcast.", config=config)

# Using custom audio (base64-encoded)
with open("background.mp3", "rb") as f:
    bg_b64 = base64.b64encode(f.read()).decode()

config = TTSConfig(
    model="zero-indic",
    voice="Nisha",
    background_audio=bg_b64,
    background_volume=0.1,  # 10% volume (subtle background)
)
result = await client.tts.synthesize("Welcome to our podcast.", config=config)
result.save("podcast_intro.mp3")
```

#### Available Speakers

Each speaker has a native language listed below, but **every speaker can speak in any language** — the native language only indicates the speaker's voice characteristics and accent.

| Language  | Male Speaker | Female Speaker |
| --------- | ------------ | -------------- |
| Assamese  | `Bimal`      | `Anjana`       |
| Bengali   | `Arjun`      | `Priyanka`     |
| Bodo      | `Daimalu`    | `Hasina`       |
| Dogri     | `Vishal`     | `Neelam`       |
| English   | `Varun`      | `Nisha`        |
| Gujarati  | `Rakesh`     | `Pooja`        |
| Hindi     | `Rajesh`     | `Sunita`       |
| Kannada   | `Kiran`      | `Shreya`       |
| Kashmiri  | `Farooq`     | `Habba`        |
| Konkani   | `Mohan`      | `Sarita`       |
| Maithili  | `Suresh`     | `Meera`        |
| Malayalam | `Krishnan`   | `Deepa`        |
| Manipuri  | `Tomba`      | `Ibemhal`      |
| Marathi   | `Siddharth`  | `Ananya`       |
| Nepali    | `Bikash`     | `Sapana`       |
| Odia      | `Bijay`      | `Sujata`       |
| Punjabi   | `Gurpreet`   | `Simran`       |
| Sanskrit  | `Vedant`     | `Gayatri`      |
| Santali   | `Chandu`     | `Roshni`       |
| Sindhi    | `Amjad`      | `Kavita`       |
| Tamil     | `Murugan`    | `Thangam`      |
| Telugu    | `Vishnu`     | `Lakshmi`      |
| Urdu      | `Salman`     | `Fatima`       |

**23 languages, 46 speakers** (1 male + 1 female per language).

#### Expression Styles

Control the emotional tone by passing a style tag before the text (e.g. `"<Happy> Hello!"`).

| Style Tag          | Description                                |
| ------------------ | ------------------------------------------ |
| `<Happy>`          | Joyful, upbeat tone                        |
| `<Sad>`            | Somber, melancholic tone                   |
| `<Angry>`          | Forceful, intense tone                     |
| `<Fearful>`        | Anxious, trembling tone                    |
| `<Surprised>`      | Exclamatory, astonished tone               |
| `<Disgust>`        | Repulsed, disapproving tone                |
| `<News>`           | Formal news-anchor style                   |
| `<Conversational>` | Casual, everyday speech                    |
| `<Narrative>`      | Storytelling / audiobook style             |
| `<Enthusiastic>`   | Energetic, passionate tone                 |
| `<Neutral>`        | Clean read-speech (default, no tag needed) |

**Expression style examples:**

```python
# Happy greeting
config = TTSConfig(model="zero-indic", voice="Rajesh")
result = await client.tts.synthesize("<Happy> Welcome aboard! We're thrilled to have you.", config=config)

# News anchor reading
config = TTSConfig(model="zero-indic", voice="Nisha")
result = await client.tts.synthesize("<News> Breaking news: the markets rallied today.", config=config)

# Storytelling
config = TTSConfig(model="zero-indic", voice="Krishnan")
result = await client.tts.synthesize("<Narrative> Once upon a time, in a land far away...", config=config)

# Conversational chatbot
config = TTSConfig(model="zero-indic", voice="Simran")
result = await client.tts.synthesize("<Conversational> Hey! How's it going?", config=config)

# Neutral (default — no tag needed)
config = TTSConfig(model="zero-indic", voice="Varun")
result = await client.tts.synthesize("Your account balance is five thousand rupees.", config=config)
```

#### Output Formats

| Format    | Value        |
| --------- | ------------ |
| PCM (raw) | `"pcm"`      |
| WAV       | `"wav"`      |
| MP3       | `"mp3"`      |
| OGG Opus  | `"ogg_opus"` |
| FLAC      | `"flac"`     |
| mu-law    | `"mulaw"`    |
| A-law     | `"alaw"`     |

#### TTS Methods

**Batch (HTTP)**

```python
result = await client.tts.synthesize("text", config=TTSConfig(...))
result.save("output.mp3")       # Save to file
result.audio_data               # Raw bytes
result.sample_rate               # Sample rate (Hz)
```

**Streaming (WebSocket)**

```python
# Iterate audio chunks
async for audio_bytes in await client.tts.stream("text", config=TTSConfig(...)):
    play(audio_bytes)

# With chunk metadata
async for chunk_meta, audio_bytes in await client.tts.stream("text", config=TTSConfig(...), detailed=True):
    print(chunk_meta.chunk_index, len(audio_bytes))

# Collect all and return combined bytes
audio = await client.tts.synthesize_stream("text", config=TTSConfig(...))

# Stream directly to file
await client.tts.stream_to_file("text", "output.pcm", config=TTSConfig(...))
```

#### `TTSResult`

Returned by `synthesize()`.

| Attribute     | Type    | Description              |
| ------------- | ------- | ------------------------ |
| `audio_data`  | `bytes` | Decoded audio bytes.     |
| `sample_rate` | `int`   | Audio sample rate in Hz. |
| `format`      | `str`   | Audio format string.     |

---

### ASR API

#### Supported Audio & Video Formats

The following formats are natively supported. For video inputs, the audio track is automatically extracted before transcription begins.

**Audio Formats**

| Format       | Extension       | Notes                                 |
| ------------ | --------------- | ------------------------------------- |
| WAV          | `.wav`          | Recommended — lossless, best accuracy |
| FLAC         | `.flac`         | Lossless compression                  |
| MP3          | `.mp3`          | Widely supported lossy format         |
| M4A / AAC    | `.m4a`, `.aac`  | Common mobile/Apple format            |
| OGG / OPUS   | `.ogg`, `.opus` | Open container / low-latency codec    |
| AMR / AMR-WB | `.amr`          | Telephony standard format             |
| AIFF         | `.aiff`         | Apple lossless audio format           |

**Video Formats**

| Format | Extension |
| ------ | --------- |
| MP4    | `.mp4`    |
| WebM   | `.webm`   |
| MOV    | `.mov`    |
| MKV    | `.mkv`    |

> ✓ For best transcription accuracy, use **WAV** files with **PCM 16-bit** encoding at **16,000 Hz mono**.

#### Sample Rate Requirements

The API supports a range of sample rates to accommodate diverse audio sources including telephony recordings. Files provided at unlisted rates are automatically resampled during processing.

| Sample Rate | Common Use Case                           |
| ----------- | ----------------------------------------- |
| 8,000 Hz    | Telephony / phone call recordings         |
| 16,000 Hz   | Standard speech recognition (recommended) |
| 22,050 Hz   | General audio                             |
| 44,100 Hz   | CD-quality audio                          |
| 48,000 Hz   | Professional broadcast audio              |

> ℹ Audio provided at sample rates not listed above is automatically resampled to 16,000 Hz during processing. No manual conversion is required.

#### `TranscriptionConfig`

Configuration for batch transcription. Passed as `config=` to `transcribe()`.

**Models:**

| Model | Use for |
|---|---|
| `zero-indic` | General Indian languages (Hindi, Tamil, Telugu, Kannada, Marathi, Bengali, etc.) |
| `zero-med` | Medical/clinical audio — auto-applies medical terminology correction |
| `zero-codeswitch` | Code-switched speech (Hinglish, Tanglish, etc.) — auto-restores mixed text |

**Core Parameters:**

| Parameter            | Type   | Default      | Description                                                              |
| -------------------- | ------ | ------------ | ------------------------------------------------------------------------ |
| `model`              | `str`  | **required** | Model name (`"zero-indic"`, `"zero-med"`, `"zero-codeswitch"`).         |
| `language_code`      | `str`  | `"auto"`     | Language hint — ISO code (`hi`, `ta`, `en`) or name (`Hindi`, `Tamil`). |
| `output_script`      | `str`  | `"auto"`     | Transliterate output script (`"Latin"`, `"Devanagari"`, `"Tamil"`, etc.). |
| `word_timestamps`    | `bool` | `False`      | Per-word start/end times and alignment confidence scores.                |

**Diarization & Speaker ID:**

| Parameter                        | Type   | Default | Description                                          |
| -------------------------------- | ------ | ------- | ---------------------------------------------------- |
| `enable_diarization`             | `bool` | `False` | Identify who spoke when (`SPEAKER_00`, `SPEAKER_01`). |
| `enable_speaker_identification`  | `bool` | `False` | Resolve speakers to registered names (requires diarization). |
| `enable_emotion_diarization`     | `bool` | `False` | Detect dominant emotion per segment.                 |
| `project`                        | `str`  | `None`  | Speaker library namespace for speaker identification. |

**NLP Features:**

| Parameter                    | Type        | Default | Description                              |
| ---------------------------- | ----------- | ------- | ---------------------------------------- |
| `enable_intent_detection`    | `bool`      | `False` | Classify transcript intent.              |
| `intent_choices`             | `list[str]` | `None`  | Constrain to specific intents.           |
| `enable_summarization`       | `bool`      | `False` | Generate a concise summary.              |
| `summary_max_length`         | `int`       | `150`   | Max words in summary.                    |
| `enable_sentiment_analysis`  | `bool`      | `False` | Sentiment label, score, and explanation. |
| `enable_keyterm_normalization` | `bool`    | `False` | Normalize domain-specific terms.         |
| `keyterm_keywords`           | `list[str]` | `None`  | Focus normalization on these terms.      |

**Post-processing:**

| Parameter                  | Type        | Default | Description                                       |
| -------------------------- | ----------- | ------- | ------------------------------------------------- |
| `enable_profanity_hashing` | `bool`      | `False` | Mask profanity with `****`.                       |
| `hash_keywords`            | `list[str]` | `None`  | Mask specific words/phrases with `****` (regex).  |
| `output_language`          | `str`       | `None`  | Translate transcript to this language (`"en"`, `"hi"`, etc.). |

#### ASR Parameter Examples

**`model` + `language_code` — Basic transcription**

```python
# Auto-detect language (default)
config = TranscriptionConfig(model="zero-indic", language_code="auto")
result = await client.asr.transcribe("audio.wav", config=config)
print(result.text)
print(f"Detected: {result.detected_language}")
# Output:
#   "Hello, how are you doing today?"
#   Detected: English

# Specify language for better accuracy on short clips
config = TranscriptionConfig(model="zero-indic", language_code="hi")
result = await client.asr.transcribe("hindi_audio.wav", config=config)
print(result.text)
# Output: "नमस्ते, आप कैसे हैं?"

# Medical audio — auto-applies medical terminology correction
config = TranscriptionConfig(model="zero-med", language_code="en")

# Code-switched speech (Hinglish, Tanglish, etc.)
config = TranscriptionConfig(model="zero-codeswitch")
```

**`output_script` — Transliterate output to a different script**

Uses [aksharamukha](https://aksharamukha.appspot.com/) — no LLM involved.

Common values: `"auto"`, `"Latin"`, `"Devanagari"`, `"Bengali"`, `"Telugu"`, `"Tamil"`, `"Kannada"`, `"ITRANS"`

```python
# Latin/Roman script — romanised output
config = TranscriptionConfig(model="zero-indic", language_code="hi", output_script="Latin")
result = await client.asr.transcribe("hindi_audio.wav", config=config)
print(result.text)
# Output: "namaste mohammad ji ye ek zaruri call hai"

# Auto (default) — no transliteration
config = TranscriptionConfig(model="zero-indic", output_script="auto")
```

**`word_timestamps` — Per-word timing and confidence**

```python
config = TranscriptionConfig(model="zero-indic", word_timestamps=True)
result = await client.asr.transcribe("audio.wav", config=config)
for seg in result.segments:
    if seg.words:
        for w in seg.words:
            print(f"  {w.word}  [{w.start:.3f}s - {w.end:.3f}s]  score={w.score}")
# Output:
#   नमस्ते  [0.532s - 0.932s]  score=-4.237
#   मोहम्मद  [1.012s - 1.412s]  score=-6.226
```

> **Note on `score`:** Log-probability from the alignment model — more negative means lower confidence. Values above `-5` are generally reliable.

**`enable_diarization` — Who spoke when**

```python
config = TranscriptionConfig(model="zero-indic", enable_diarization=True)
result = await client.asr.transcribe("call.wav", config=config)
print(result.speakers)  # ["SPEAKER_00", "SPEAKER_01"]
for seg in result.segments:
    print(f"  [{seg.speaker}] [{seg.start:.1f}s - {seg.end:.1f}s] {seg.text}")
# Output:
#   [SPEAKER_00] [0.5s - 3.2s] नमस्ते, आप कैसे हैं
#   [SPEAKER_01] [4.1s - 6.8s] मैं ठीक हूँ धन्यवाद
```

**`enable_speaker_identification` + `project` — Resolve to registered names**

Requires `enable_diarization=True` and pre-registered voice profiles (see Speaker APIs).

```python
config = TranscriptionConfig(
    model="zero-indic",
    enable_diarization=True,
    enable_speaker_identification=True,
    project="support_team",
)
result = await client.asr.transcribe("call.wav", config=config)
print(result.speakers)  # ["Priya", "Rahul"]
for seg in result.segments:
    print(f"  [{seg.speaker}] {seg.text}")
# Output:
#   [Priya] नमस्ते, आप कैसे हैं
#   [Rahul] मैं ठीक हूँ
```

**`enable_emotion_diarization` — Emotion per segment**

```python
config = TranscriptionConfig(
    model="zero-indic",
    enable_diarization=True,
    enable_emotion_diarization=True,
)
result = await client.asr.transcribe("call.wav", config=config)
for seg in result.segments:
    print(f"  [{seg.speaker}] ({seg.emotion}) {seg.text}")
# Output:
#   [SPEAKER_00] (angry) I've been waiting for an hour!
#   [SPEAKER_01] (neutral) I'm sorry about that, let me help.
```

**`enable_intent_detection` + `intent_choices` — Classify intent**

```python
# Constrained to specific choices
config = TranscriptionConfig(
    model="zero-indic",
    enable_intent_detection=True,
    intent_choices=["complaint", "inquiry", "service_request", "compliment"],
)
result = await client.asr.transcribe("customer_call.wav", config=config)
print(result.nlp_analysis.intent)
# Output: {"label": "service_request", "confidence": 0.92, "reasoning": "Caller is requesting roadside assistance"}

# Open intent — no constraints
config = TranscriptionConfig(model="zero-indic", enable_intent_detection=True)
```

**`enable_summarization` + `summary_max_length` — Summarize transcript**

```python
config = TranscriptionConfig(
    model="zero-indic",
    enable_summarization=True,
    summary_max_length=50,  # max ~50 words
)
result = await client.asr.transcribe("meeting.wav", config=config)
print(result.nlp_analysis.summary)
# Output: "Customer called about a vehicle breakdown. Agent confirmed the complaint and promised a technician within the hour."
```

**`enable_sentiment_analysis` — Detect sentiment**

```python
config = TranscriptionConfig(model="zero-indic", enable_sentiment_analysis=True)
result = await client.asr.transcribe("feedback.wav", config=config)
print(result.nlp_analysis.sentiment)
# Output: {"label": "negative", "score": -0.72, "explanation": "Customer expresses frustration about..."}
```

**`enable_keyterm_normalization` + `keyterm_keywords` — Fix domain terms**

```python
config = TranscriptionConfig(
    model="zero-indic",
    enable_keyterm_normalization=True,
    keyterm_keywords=["EMI", "NACH mandate", "bounce charge"],
)
result = await client.asr.transcribe("finance_call.wav", config=config)
print(result.nlp_analysis.normalized_text)
# Output: "आपकी EMI की तारीख पाँच अगस्त है" (normalized from "emi")
```

**`enable_profanity_hashing` + `hash_keywords` — Redact sensitive words**

```python
# Mask profanity
config = TranscriptionConfig(model="zero-indic", enable_profanity_hashing=True)
result = await client.asr.transcribe("audio.wav", config=config)
# Output: "अरे **** यह काम क्यों नहीं हो रहा"

# Mask specific keywords (regex, case-insensitive, no LLM)
config = TranscriptionConfig(
    model="zero-indic",
    hash_keywords=["account number", "card number", "OTP"],
)
result = await client.asr.transcribe("call.wav", config=config)
# Output: "आपका **** 4321 है और आपका **** कल भेजा गया था"
```

**`output_language` — Translate transcript**

```python
config = TranscriptionConfig(
    model="zero-indic",
    language_code="hi",
    output_language="en",
)
result = await client.asr.transcribe("hindi_audio.wav", config=config)
print(result.text)
# Output: "Hello, this is an urgent call regarding your vehicle service."
print(result.nlp_analysis.translation)
# Also available in nlp_analysis
```

#### ASR Methods

**Batch (HTTP)**

The `transcribe()` method accepts multiple input types:

```python
# From file path (string)
result = await client.asr.transcribe("audio.wav", config=TranscriptionConfig(model="zero-indic"))
result = await client.asr.transcribe("/absolute/path/to/recording.mp3", config=TranscriptionConfig(model="zero-indic"))

# From raw audio bytes (in-memory audio)
with open("audio.wav", "rb") as f:
    audio_bytes = f.read()
result = await client.asr.transcribe(audio_bytes, config=TranscriptionConfig(model="zero-indic"))

# From file object
with open("audio.wav", "rb") as f:
    result = await client.asr.transcribe_file(f, config=TranscriptionConfig(model="zero-indic"))

# From remote URL
result = await client.asr.transcribe_url("https://example.com/audio.wav", config=TranscriptionConfig(model="zero-indic"))
```

> ⚠ URL inputs must be publicly accessible at the time of the API request. Authenticated or signed URLs with short expiry times may fail.

**Streaming (WebSocket)**

```python
conn = await client.asr.stream(config=StreamingConfig(language="en"))
```

#### `StreamingConfig`

Configuration for the WebSocket streaming session.

| Parameter               | Type    | Default   | Description                               |
| ----------------------- | ------- | --------- | ----------------------------------------- |
| `language`              | `str`   | `"auto"`  | Language code or `"auto"`.                |
| `sample_rate`           | `int`   | `16000`   | Audio sample rate in Hz.                  |
| `dtype`                 | `str`   | `"int16"` | Audio data type (`"int16"`, `"float32"`). |
| `chunk_size_sec`        | `float` | `1.0`     | Processing chunk size in seconds.         |
| `silence_threshold_sec` | `float` | `0.5`     | Silence duration to trigger segmentation. |

#### Streaming Parameter Examples

**`language` — Set recognition language**

```python
# Auto-detect (default)
conn = await client.asr.stream(config=StreamingConfig(language="auto"))

# Specific language for better accuracy
conn = await client.asr.stream(config=StreamingConfig(language="en"))
conn = await client.asr.stream(config=StreamingConfig(language="hi"))
conn = await client.asr.stream(config=StreamingConfig(language="ta"))
```

**`sample_rate` + `dtype` — Match your audio source**

```python
# Standard microphone input: 16kHz, 16-bit integer (default)
conn = await client.asr.stream(config=StreamingConfig(
    sample_rate=16000,
    dtype="int16",
))

# High-quality audio: 48kHz, 32-bit float
conn = await client.asr.stream(config=StreamingConfig(
    sample_rate=48000,
    dtype="float32",
))
```

**`chunk_size_sec` — Processing window size**

```python
# Smaller chunks = lower latency, more partial results
conn = await client.asr.stream(config=StreamingConfig(chunk_size_sec=0.5))

# Larger chunks = more context, potentially better accuracy
conn = await client.asr.stream(config=StreamingConfig(chunk_size_sec=2.0))
```

**`silence_threshold_sec` — Control segment boundaries**

```python
# Quick segmentation — short pauses trigger a new segment
conn = await client.asr.stream(config=StreamingConfig(silence_threshold_sec=0.3))
# Good for: fast-paced dialogue, command recognition

# Patient segmentation — only split on longer pauses
conn = await client.asr.stream(config=StreamingConfig(silence_threshold_sec=1.5))
# Good for: lectures, monologues, dictation
```

#### Streaming Events

Register event handlers on the connection object:

```python
conn = await client.asr.stream(config=StreamingConfig(language="en"))

@conn.on(StreamingMessageType.PARTIAL)
def on_partial(msg):
    print(f"Interim: {msg.text}")

@conn.on(StreamingMessageType.FINAL_SEGMENT)
def on_segment(msg):
    print(f"Segment: {msg.text}")

@conn.on(StreamingMessageType.FINAL)
def on_final(msg):
    print(f"Final: {msg.text} ({msg.audio_duration_sec}s)")

@conn.on(StreamingMessageType.DONE)
def on_done(msg):
    print(f"Done. {msg.total_segments} segments, {msg.total_audio_duration_sec}s")

@conn.on(StreamingMessageType.ERROR)
def on_error(msg):
    print(f"Error: {msg.message}")
```

| Event           | Model                   | Key Attributes                                                |
| --------------- | ----------------------- | ------------------------------------------------------------- |
| `PARTIAL`       | `StreamingPartial`      | `text`, `language`, `segment_id`, `latency_ms`                |
| `FINAL_SEGMENT` | `StreamingFinalSegment` | `text`, `language`, `segment_id`, `silence_duration_ms`       |
| `FINAL`         | `StreamingFinal`        | `text`, `language`, `audio_duration_sec`, `inference_time_ms` |
| `DONE`          | `StreamingDone`         | `total_segments`, `total_audio_duration_sec`                  |
| `ERROR`         | `StreamingError`        | `message`, `code`                                             |

#### Streaming Connection Methods

```python
await conn.send_audio(pcm_bytes)   # Send raw PCM audio
await conn.end()                    # Signal end of audio stream
await conn.close()                  # Close WebSocket connection
conn.is_closed                      # Check connection status
conn.session_id                     # Server-assigned session ID
```

#### `TranscriptionResult`

Returned by `transcribe()`.

**Response Schema**

```json
{
  "success": true,
  "request_id": "a1b2c3...",
  "text": "[Priya] नमस्ते मेरी गाड़ी खराब हो गई है [Rahul] ठीक है मैं आपकी मदद करता हूँ",
  "segments": [
    {
      "start": 0.5,
      "end": 4.2,
      "text": "नमस्ते मेरी गाड़ी खराब हो गई है",
      "speaker": "Priya",
      "emotion": "sad",
      "words": [
        { "word": "नमस्ते", "start": 0.53, "end": 0.93, "score": -4.2 }
      ]
    }
  ],
  "detected_language": "hindi",
  "speakers": ["Priya", "Rahul"],
  "audio_duration": 9.1,
  "inference_time_ms": 3241.5,
  "nlp_analysis": {
    "intent": { "label": "service_request", "confidence": 0.95, "reasoning": "..." },
    "summary": "Customer reported a vehicle breakdown. Agent offered assistance.",
    "sentiment": { "label": "negative", "score": -0.6, "explanation": "..." },
    "normalized_text": "...",
    "translation": "..."
  }
}
```

**Accessing Fields**

```python
result = await client.asr.transcribe("audio.wav", config=TranscriptionConfig(model="zero-indic"))

# Full transcript
print(result.text)

# Detected language
print(result.detected_language)

# Speakers (when diarization is enabled)
print(result.speakers)  # ["SPEAKER_00", "SPEAKER_01"] or ["Priya", "Rahul"]

# Iterate timestamped segments
for seg in result.segments:
    speaker = f"[{seg.speaker}] " if seg.speaker else ""
    emotion = f"({seg.emotion}) " if seg.emotion else ""
    print(f"{speaker}{emotion}[{seg.start:.2f}s → {seg.end:.2f}s]  {seg.text}")

    # Per-word timestamps (when word_timestamps=True)
    if seg.words:
        for w in seg.words:
            print(f"    {w.word} [{w.start:.3f}s-{w.end:.3f}s] score={w.score}")
```

| Attribute           | Type                  | Description                                                        |
| ------------------- | --------------------- | ------------------------------------------------------------------ |
| `success`           | `bool`                | Whether transcription succeeded.                                   |
| `request_id`        | `str`                 | Unique request identifier.                                         |
| `text`              | `str`                 | Full transcription text (prefixed with `[SPEAKER_XX]` when diarized). |
| `segments`          | `list[SegmentResult]` | Time-aligned segments (see below).                                 |
| `detected_language` | `str`                 | Detected language name (e.g. `"hindi"`, `"English"`).              |
| `speakers`          | `list[str]`           | Unique speaker labels (empty when diarization is off).             |
| `audio_duration`    | `float`               | Audio duration in seconds.                                         |
| `inference_time_ms` | `float`               | Server inference time in ms.                                       |
| `nlp_analysis`      | `NLPAnalysis`         | NLP results (if any `enable_*` flags were set).                    |

#### `SegmentResult`

| Attribute | Type              | Description                                          |
| --------- | ----------------- | ---------------------------------------------------- |
| `start`   | `float`           | Segment start time in seconds.                       |
| `end`     | `float`           | Segment end time in seconds.                         |
| `text`    | `str`             | Transcribed text for this segment.                   |
| `speaker` | `str` or `None`   | Speaker label (when diarization is enabled).         |
| `emotion` | `str` or `None`   | Emotion label (when emotion diarization is enabled). |
| `words`   | `list[WordResult]` or `None` | Per-word timestamps (when `word_timestamps=True`). |

#### `WordResult`

| Attribute | Type    | Description                                          |
| --------- | ------- | ---------------------------------------------------- |
| `word`    | `str`   | The word.                                            |
| `start`   | `float` | Word start time in seconds.                          |
| `end`     | `float` | Word end time in seconds.                            |
| `score`   | `float` or `None` | Log-probability confidence (more negative = less confident). |

#### `NLPAnalysis`

| Attribute          | Type   | Description                                    |
| ------------------ | ------ | ---------------------------------------------- |
| `intent`           | `dict` | Intent label, confidence, reasoning.           |
| `summary`          | `str`  | Transcript summary.                            |
| `sentiment`        | `dict` | Sentiment label, score, explanation.           |
| `emotion`          | `dict` | Emotion detection results.                     |
| `translation`      | `str`  | Translated text (when `output_language` is set). |
| `normalized_text`  | `str`  | Text with normalized key terms.                |

---

### Exceptions

All exceptions inherit from `ShunyalabsError`.

| Exception               | Description                                    |
| ----------------------- | ---------------------------------------------- |
| `AuthenticationError`   | Invalid or missing API key (401).              |
| `PermissionDeniedError` | Insufficient permissions (403).                |
| `NotFoundError`         | Resource not found (404).                      |
| `RateLimitError`        | Rate limit exceeded (429).                     |
| `ServerError`           | Server-side error (5xx).                       |
| `TimeoutError`          | Request timed out.                             |
| `ConnectionError`       | Network connectivity issue.                    |
| `ConfigurationError`    | Invalid or missing configuration.              |
| `AudioError`            | Invalid, corrupted, or unsupported audio data. |
| `SessionError`          | Invalid or expired session state.              |
| `ConversationError`     | Error returned during conversation flow.       |
| `TranscriptionError`    | ASR-specific transcription error.              |
| `SynthesisError`        | TTS-specific synthesis error.                  |

**Exception handling example:**

```python
from shunyalabs import (
    ConfigurationError,
    AuthenticationError,
    ConnectionError,
    TranscriptionError,
    AudioError,
    SessionError,
    TimeoutError,
    ConversationError,
)

try:
    result = await client.asr.transcribe("audio.wav", config=TranscriptionConfig(model="zero-indic"))
    print(result.text)

except ConfigurationError as e:
    print(f"Invalid configuration: {e}")

except AuthenticationError as e:
    print(f"Authentication failed: {e}")

except ConnectionError as e:
    print(f"Unable to connect: {e}")

except TranscriptionError as e:
    print(f"Transcription processing failed: {e}")

except AudioError as e:
    print(f"Invalid/unsupported audio: {e}")

except SessionError as e:
    print(f"Invalid or expired session: {e}")

except TimeoutError as e:
    print(f"Request timed out: {e}")

except ConversationError as e:
    print(f"Conversation error: {e}")

except Exception as e:
    print(f"Unexpected error: {e}")
```

---

---

## File Duration & Rate Limits

| Limit                  | Value      |
| ---------------------- | ---------- |
| Maximum audio duration | 15 minutes |
| Maximum File size      | 50 MB      |
| Concurrent requests    | 16         |

> ⚠ For files exceeding the duration limit, split the audio into smaller segments before submitting. Consider using silence-detection tools to identify natural break points.

> ℹ Support for pre-signed URLs for large audio files will be introduced in an upcoming beta release, allowing clients to provide a secure URL instead of uploading directly.

---

## ASR End-to-End Quickstart

A complete integration walkthrough from installation to parsing the response.

### Step 1 — Install the SDK

```bash
pip install shunyalabsai[all]
```

### Step 2 — Authenticate

```python
import os
from shunyalabs import AsyncShunyaClient

client = AsyncShunyaClient(api_key=os.environ.get("SHUNYALABS_API_KEY"))
```

> ⚠ Store your API key in an environment variable rather than hardcoding it.

### Step 3 — Transcribe Audio

```python
from shunyalabs.asr import TranscriptionConfig

async with AsyncShunyaClient(api_key=os.environ.get("SHUNYALABS_API_KEY")) as client:
    result = await client.asr.transcribe(
        "recording.wav",
        config=TranscriptionConfig(model="zero-indic"),
    )
```

### Step 4 — Parse & Use the Response

```python
# Full transcript text
print("Transcript:", result.text)

# Detected language
print("Language:", result.detected_language)

# Timestamped segments
for seg in result.segments:
    print(f"  [{seg.start:.2f}s – {seg.end:.2f}s]  {seg.text}")
```

### Complete Example

```python
import asyncio, os
from shunyalabs import AsyncShunyaClient, AuthenticationError
from shunyalabs.asr import TranscriptionConfig

async def main():
    async with AsyncShunyaClient(api_key=os.environ.get("SHUNYALABS_API_KEY")) as client:
        config = TranscriptionConfig(
            model="zero-indic",
            enable_diarization=True,
            word_timestamps=True,
            enable_sentiment_analysis=True,
        )
        try:
            result = await client.asr.transcribe("customer_call.wav", config=config)

            print("=== Transcript ===")
            print(result.text)
            print(f"Detected language: {result.detected_language}")
            print(f"Speakers: {result.speakers}")
            print()
            print("=== Segments ===")
            for seg in result.segments:
                speaker = f"[{seg.speaker}] " if seg.speaker else ""
                print(f"{speaker}[{seg.start:.2f}s → {seg.end:.2f}s]  {seg.text}")

            if result.nlp_analysis and result.nlp_analysis.sentiment:
                print(f"\nSentiment: {result.nlp_analysis.sentiment}")

        except AuthenticationError as e:
            print(f"Authentication failed: {e}")
        except Exception as e:
            print(f"Error: {e}")

asyncio.run(main())
```

---

## Development

```bash
git clone https://github.com/Shunyalabsai/shunyalabs-python-sdk.git
cd shunyalabs-python-sdk

python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Run tests
pytest

# Lint
ruff check src/
black --check src/
mypy src/
```

## License

[MIT](LICENSE)
