> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ringg.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> To search or read these docs, connect the Ringg docs MCP server at https://docs.ringg.ai/mcp (no key needed; tools search_ringg_ai and query_docs_filesystem_ringg_ai) instead of browsing pages one by one. Setup for each client: https://docs.ringg.ai/resources/mcp-server/docs-mcp-server/mcp-for-ai-assistants.md
> The dashboard labels agents "Assistants"; these docs say "agent". Both mean the same object, identified by agent_id.
> API base URL: https://prod-api.ringg.ai/ca/api/v0. Send the workspace API key in the X-API-KEY header, from server-side code only.
> Placing a call dials a real phone number and spends credits. Confirm the number with the user before calling.
> To act on a Ringg workspace (place calls, edit agents, read transcripts), use the separate Ringg workspace MCP server at https://mcp.ringg.ai/mcp: https://docs.ringg.ai/resources/mcp-server/workspace-mcp-server/overview.md

# Parrot STT Python SDK

> Transcribe audio files or stream live audio in Hindi and English with the Parrot STT Python SDK, with every parameter, event and error, and the STT dashboard.

The `ringglabs` Python SDK sends a complete audio file in one REST request, or streams live audio over a WebSocket and returns transcripts while the speaker talks; every feature works with both a synchronous `Client` and an `AsyncClient`.

Parrot <Tooltip tip="Speech-to-text: turning the caller’s audio into text.">STT</Tooltip> is in beta: SDK `v0.1.x`, MIT licence, Python 3.10 or later. Parameters and event fields can change before general availability.

| Feature | Support |
| - | - |
| Languages | Hindi (`hi`, the default) and English (`en`) |
| File formats | WAV, MP3, FLAC, M4A |
| File size | Up to 10 MB per file |
| Streaming audio | Raw mono PCM, 8,000 to 48,000 Hz, `int16`, `linear16`, `int32` or `float32` |
| Billing | A streaming rate per hour of audio, in your workspace currency; see the **Pricing** tab below |

## Before you start

<Columns cols={2}>
  <Card title="A workspace API key" icon="key" href="/documentation/manage-account/api-key" horizontal>
    The SDK sends it on every request; store it as `RINGG_API_KEY`.
  </Card>

  <Card title="Balance" icon="wallet" href="/documentation/manage-account/billing" horizontal>
    At least one minute of credit to start a transcription.
  </Card>
</Columns>

## Try it in the dashboard

**Sidebar → Labs → STT** (page heading **Speech To Text**, marked **BETA**) transcribes without code and shows your usage. Its **API docs** button opens the SDK on PyPI. The page has four tabs:

| Tab | What it holds |
| - | - |
| **Quick start** | The try-it panel and the install, key and first-call steps below |
| **Logs** | Every request, 50 per page |
| **Usage** | **Total requests**, **Audio processed**, **Credits used**, and **Daily credit usage** for the past 14 days |
| **Pricing** | Your **Streaming rate** per hour, the 30 included concurrent streams, and **Contact sales** for more |

**Logs** lists **Request ID**, **Timestamp**, **Duration**, **Mode**, **Credits** and **Status**, filtered by **Status** (**In progress**, **Done**, **Failed**), **Mode** (**Streaming**, **On Final**, **REST**) and a date range. Click a row to open the **STT Request** drawer with the request's **Parameters** and **Latency**.

In the try-it panel, pick **Language** (**English** or **Hindi**; Hindi is the default), switch **Punctuation** on if you want capitalisation and punctuation (off by default here; the SDK defaults it to on), then **Upload file** (WAV, MP3, FLAC or M4A, up to 10 MB) or **Transcribe live** from your microphone. The result shows on **Transcription**; **Code** shows the same request as TypeScript, Python and cURL.

<Frame caption="The try-it panel after uploading a file">
  <img src="https://mintcdn.com/ringgai-0da73d20/XUOcNie5hfK3q--Y/media/parrot-stt/try-it.webp?fit=max&auto=format&n=XUOcNie5hfK3q--Y&q=85&s=8b82c03ae39546ad40a3fff986e25165" alt="STT try-it panel with Language English, Punctuation on, Upload file and Transcribe live buttons, and a Transcription box showing a transcribed Acme Lending payment reminder with a 6.2s duration chip" width="2336" height="564" data-path="media/parrot-stt/try-it.webp" />
</Frame>

## Quickstart

The video ends at the API key the SDK needs; the steps below start there.

<Frame caption="A tour of the STT page: file upload, then the Logs, Usage and Pricing tabs and the API key">
  <video alt="A tour of the STT page: file upload, then the Logs, Usage and Pricing tabs and the API key" aria-label="A tour of the STT page: file upload, then the Logs, Usage and Pricing tabs and the API key" controls muted playsInline className="w-full rounded-xl" style={{ aspectRatio: "16 / 10" }} preload="metadata" controlsList="nodownload noremoteplayback" poster="/media/parrot-stt/poster.webp">
    <source src="https://mintcdn.com/ringgai-0da73d20/XUOcNie5hfK3q--Y/media/parrot-stt/video.webm?fit=max&auto=format&n=XUOcNie5hfK3q--Y&q=85&s=17c501a6d8066c61158dfb29a49ad487" type="video/webm; codecs=&#x22;av01.0.09M.08&#x22;" data-path="media/parrot-stt/video.webm" />
  </video>
</Frame>

<Steps>
  <Step title="Install">
    Add the SDK to your environment:

    ```bash theme={null}
    pip install ringglabs
    ```
  </Step>

  <Step title="Get an API key">
    The SDK uses your workspace [API key](/documentation/manage-account/api-key), generated under **Settings → API key**. Store it as `RINGG_API_KEY`.

    The workspace needs at least one minute of credit to start a transcription.

    <Frame caption="The key is shown once, when generated">
      <img src="https://mintcdn.com/ringgai-0da73d20/XUOcNie5hfK3q--Y/media/parrot-stt/api-key.webp?fit=max&auto=format&n=XUOcNie5hfK3q--Y&q=85&s=d31220bb281b10651699361c39a401e3" alt="Generate API Key dialog showing a demo key, a Copy button and a warning that the key cannot be viewed again after leaving the page" width="1120" height="664" data-path="media/parrot-stt/api-key.webp" />
    </Frame>
  </Step>

  <Step title="First call">
    Transcribe a local audio file:

    ```python theme={null}
    import os
    from ringglabs.stt import Client

    with Client(api_key=os.environ["RINGG_API_KEY"]) as client:
        result = client.transcribe("sample.wav", language="en")
        print(result.transcription)
    ```
  </Step>
</Steps>

## SDK implementation

Pick the example that matches your audio source and execution model. The streaming examples read a WAV file and send it in 20 ms chunks, using two helpers from [Audio helpers](#audio-helpers); for live audio, send each chunk as it is captured instead (20 to 40 ms chunks work best).

<Tabs>
  <Tab title="Sync stream">
    ```python theme={null}
    import os
    from ringglabs.stt import Client, TimeoutError as SdkTimeoutError
    from audio_helpers import load_wav_mono_int16_16k, iter_pcm_chunks

    sample_rate, audio = load_wav_mono_int16_16k("sample.wav")
    transcripts = []

    with Client(api_key=os.environ["RINGG_API_KEY"]) as client, client.stream(
        sample_rate=sample_rate, encoding="int16", language="en",
        mode="stream", enable_cap_punc=True,
    ) as session:
        for chunk in iter_pcm_chunks(audio, sample_rate, 20):
            session.send_audio(chunk)
        session.end()

        try:
            for event in session.events():
                if event.type == "transcript" and event.transcription.strip():
                    transcripts.append(event.transcription.strip())
        except SdkTimeoutError:
            pass

    print(transcripts)
    ```
  </Tab>

  <Tab title="Async stream">
    ```python theme={null}
    import asyncio
    import os
    from ringglabs.stt import AsyncClient, TimeoutError as SdkTimeoutError
    from audio_helpers import load_wav_mono_int16_16k, iter_pcm_chunks

    async def main():
        sample_rate, audio = load_wav_mono_int16_16k("sample.wav")

        async with AsyncClient(api_key=os.environ["RINGG_API_KEY"]) as client, client.stream(
            sample_rate=sample_rate, encoding="int16", language="en",
            mode="stream", enable_cap_punc=True,
        ) as session:
            for chunk in iter_pcm_chunks(audio, sample_rate, 20):
                await session.send_audio(chunk)
            await session.end()

            try:
                async for event in session.events():
                    if event.type == "transcript":
                        print(event.transcription)
            except SdkTimeoutError:
                pass

    asyncio.run(main())
    ```
  </Tab>

  <Tab title="On-final">
    ```python theme={null}
    import os
    from ringglabs.stt import Client, TimeoutError as SdkTimeoutError
    from audio_helpers import load_wav_mono_int16_16k, iter_pcm_chunks

    sample_rate, audio = load_wav_mono_int16_16k("sample.wav")
    partials, finals = [], []

    with Client(api_key=os.environ["RINGG_API_KEY"]) as client, client.stream(
        sample_rate=sample_rate, encoding="int16", language="en",
        mode="on_final", accept_client_vad_events=True,
    ) as session:
        session.start_speaking()
        for chunk in iter_pcm_chunks(audio, sample_rate, 20):
            session.send_audio(chunk)
        session.stop_speaking()
        session.end()

        try:
            for event in session.events():
                if event.type != "transcript":
                    continue
                text = event.transcription.strip()
                (finals if event.is_final else partials).append(text)
        except SdkTimeoutError:
            pass

    print(finals)
    ```
  </Tab>

  <Tab title="Offline">
    ```python theme={null}
    import os
    from ringglabs.stt import Client

    with Client(api_key=os.environ["RINGG_API_KEY"]) as client:
        result = client.transcribe(
            "sample.wav",
            language="en",
            enable_cap_punc=True,
        )
        print(result.request_id, result.transcription)
    ```
  </Tab>

  <Tab title="Bytes / IO">
    ```python theme={null}
    import os
    from io import BytesIO
    from pathlib import Path
    from ringglabs.stt import Client

    wav_bytes = Path("sample.wav").read_bytes()

    with Client(api_key=os.environ["RINGG_API_KEY"]) as client:
        # bytes source
        r1 = client.transcribe(wav_bytes, filename="sample.wav")

        # file-like source
        r2 = client.transcribe(BytesIO(wav_bytes), filename="sample.wav")
    ```
  </Tab>
</Tabs>

### Audio helpers

The streaming examples import these from `audio_helpers.py`. They use only the Python standard library.

<Accordion title="audio_helpers.py" icon="file-code">
  ```python theme={null}
  import wave

  def load_wav_mono_int16_16k(path):
      """Read a 16 kHz, mono, 16-bit PCM WAV file. Returns (sample_rate, pcm_bytes)."""
      with wave.open(str(path), "rb") as wav:
          if (wav.getframerate(), wav.getnchannels(), wav.getsampwidth()) != (16000, 1, 2):
              raise ValueError("Expected a 16 kHz, mono, 16-bit PCM WAV file")
          return wav.getframerate(), wav.readframes(wav.getnframes())

  def iter_pcm_chunks(pcm, sample_rate, chunk_ms=20):
      """Yield raw int16 PCM in chunks of chunk_ms milliseconds."""
      chunk_bytes = sample_rate * chunk_ms // 1000 * 2  # 2 bytes per int16 sample
      for start in range(0, len(pcm), chunk_bytes):
          yield pcm[start:start + chunk_bytes]
  ```

  To convert another file to this format, run `ffmpeg -i input.mp3 -ac 1 -ar 16000 -sample_fmt s16 sample.wav`.
</Accordion>

## Client initialization

`Client(...)` and `AsyncClient(...)` share the same constructor. Both can be used as context managers (`with` / `async with`), which close the underlying connections on exit.

| Parameter | Default | Description |
| - | - | - |
| `base_url` | `"prod-api.ringg.ai"` | Host name. |
| `api_key` | `None` | Default API key for all requests; can be overridden per call. |
| `timeout` | `TimeoutConfig()` | HTTP and WebSocket timeout budgets. See [Timeouts](#timeouts). |
| `default_headers` | `None` | Extra headers attached to SDK requests. |

## transcribe() (offline / REST)

Single-shot transcription of a complete audio file. Available on both `Client` and `AsyncClient` (`await client.transcribe(...)`).

| Parameter | Default | Description |
| - | - | - |
| `source` | | Input audio: `str`, `Path`, `bytes`, `bytearray`, or `BinaryIO`. |
| `language` | `"hi"` | Language of the audio: `"hi"` or `"en"`. |
| `enable_cap_punc` | `True` | Enable capitalization and punctuation. |
| `api_key` | `None` | Per-request API key override. |
| `filename` | auto-detected | Multipart filename, used to detect the audio format. Falls back to `"audio.wav"`. Set it when passing `bytes` or a stream that isn't WAV. |
| `content_type` | `"audio/wav"` | Multipart content type. |

### Response: `RestTranscriptionResult`

| Field | Type | Description |
| - | - | - |
| `status` | `str` | `"success"` when the file was transcribed. |
| `transcription` | `str` | Full transcript of the file. |
| `is_final` | `bool` | Always `True` for file transcription. |
| `language` | `str` | Language used for transcription. |
| `duration_seconds` | `float` | Length of the transcribed audio, in seconds. |
| `processing_time_seconds` | `float` | Server processing time, in seconds. |
| `request_id` | `str` | Unique ID for the request. Include it when contacting support. |
| `raw` | `dict` | The unparsed JSON response. |

<Frame caption="A request opened from the Logs tab by its request_id">
  <img src="https://mintcdn.com/ringgai-0da73d20/XUOcNie5hfK3q--Y/media/parrot-stt/log-detail.webp?fit=max&auto=format&n=XUOcNie5hfK3q--Y&q=85&s=e8aaa3d6067ab7ddf68c9020d8dfbcb6" alt="STT Request drawer for a Done streaming request: timestamp, duration 47.8s, mode Streaming, credits 0.40 rupees, and a Latency tab listing audio duration, compute latency, processing time and segments" width="1024" height="1010" data-path="media/parrot-stt/log-detail.webp" />
</Frame>

## stream() (real-time / WebSocket)

Open a streaming session, send audio chunks, and receive transcript events. Use it as a context manager so the session is opened on entry and closed on exit.

| Parameter | Default | Description |
| - | - | - |
| `sample_rate` | `16000` | Audio sample rate in Hz, from 8000 to 48000. |
| `encoding` | `"int16"` | `int16`, `linear16`, `float32`, or `int32`. Audio must be raw mono PCM with no file header. |
| `language` | `"hi"` | Language hint: `"hi"` or `"en"`. |
| `mode` | `"stream"` | `"stream"` or `"on_final"`. See below. |
| `vad_tail_sil_ms` | `200` | Server <Tooltip tip="Voice activity detection: how the agent decides the caller has started or stopped speaking.">VAD</Tooltip> tail silence in ms: how long a pause ends a segment. |
| `vad_confidence` | `0.55` | Server VAD confidence threshold. |
| `enable_cap_punc` | `True` | Capitalization / punctuation. |
| `accept_client_vad_events` | `False` | Enables `start_speaking()` / `stop_speaking()`. Only applies in `on_final` mode. |
| `api_key` | `None` | Per-stream API key override. |

### Stream and on-final modes

* **`stream`**: the server detects pauses with its own voice activity detection (VAD) and sends a final `transcript` event for each spoken segment. Join the segments to build the full transcript. The session's closing `transcript` event can have an empty `transcription`, so skip blank ones.
* **`on_final`**: you mark each turn with `start_speaking()` and `stop_speaking()` (requires `accept_client_vad_events=True`). The server sends partial transcripts (`is_final=False`) that grow as the turn continues, then one final transcript (`is_final=True`) after `stop_speaking()`.

### Session methods

| Method | Purpose |
| - | - |
| `send_audio(bytes)` | Send a raw audio chunk. |
| `send_vad_event(state)` | Send an explicit VAD event: `"user_start_speaking"` or `"user_stop_speaking"`. |
| `start_speaking()` / `stop_speaking()` | Convenience wrappers for VAD events. |
| `ping()` | Send a ping frame. The SDK also pings automatically every 30 seconds. |
| `end(command="end")` | Finalize / terminate the stream. |
| `recv_event()` / `events()` | Receive parsed server events. `events()` stops when the server closes the session. |
| `close()` | Close the WebSocket session. |

The async session is identical, with `await` on every call and `async for` on `events()`.

### WebSocket event types

| Type | When it is sent |
| - | - |
| `ready` | Once, when the session opens. Confirms the effective settings and the session's `request_id`. The SDK reads it during the handshake and exposes it as `session.ready_event`. |
| `transcript` | A transcription result. See the fields below. |
| `ack` | Acknowledges a VAD event or `end`. `ack_for` names the message and `state` gives the result. |
| `pong` | Reply to `ping()`, with a server `timestamp`. |
| `error` | The server rejected a message or ended the session. Read `code` and `detail`. |

### Transcript event fields

| Field | Description |
| - | - |
| `transcription` | Transcribed text for this segment or turn. |
| `is_final` | `True` when the text will not change. |
| `language` | Language used for transcription. |
| `request_id` | Session ID. Include it when contacting support. |
| `segment_idx` | Position of this segment within the session. |
| `segments` | Number of segments, on the closing event. |
| `compute_latency_ms` | Model compute time for this segment, in ms. |
| `audio_duration_sec` | Audio received in the session, in seconds. |
| `transcribed_audio_duration_sec` | Audio that was transcribed, in seconds. |
| `processing_time_ms` | Total processing time, in ms. |
| `raw` | The unparsed JSON event. |

Fields that don't apply to an event are `None`.

## Timeouts

Pass a `TimeoutConfig` to `Client(timeout=...)`. All values are in seconds.

| Parameter | Default | Description |
| - | - | - |
| `connect` | `10.0` | HTTP connect. |
| `read` | `10.0` | HTTP read. |
| `write` | `10.0` | HTTP write. |
| `pool` | `10.0` | HTTP pool. |
| `ws_open` | `10.0` | WebSocket open. |
| `ws_recv` | `30.0` | WebSocket receive. `events()` raises `TimeoutError` if no event arrives in this time. |
| `ws_close` | `10.0` | WebSocket close. |

```python theme={null}
import os
from ringglabs.stt import Client, TimeoutConfig

client = Client(
    api_key=os.environ["RINGG_API_KEY"],
    timeout=TimeoutConfig(read=60.0, ws_recv=60.0),
)
```

## Errors

All SDK exceptions are importable from `ringglabs.stt` and inherit from `RinggLabsError`.

| Exception | Raised when |
| - | - |
| `ValidationError` | An argument is invalid, for example a file path that doesn't exist. |
| `ApiError` | The server returns an error. Has `message`, `status_code`, `code`, and `payload`. |
| `AuthenticationError` | The API key is missing or invalid (`401`), or the workspace doesn't have enough credit (`402`). Subclass of `ApiError`. |
| `TransportError` | A network failure. |
| `TimeoutError` | A request or receive exceeded its [timeout](#timeouts). Subclass of `TransportError`. |
| `ProtocolError` | The server sent a malformed or unexpected message. |

A file over 10 MB is rejected with an `ApiError` (`status_code=413`), and a file in another format with `status_code=415` (`code` `UNSUPPORTED_AUDIO_FORMAT`). A streaming session ends after one hour with the error code `MAX_DURATION_EXCEEDED`, and after 5 minutes without a message from the client with `SESSION_TIMEOUT`.

## Recommended practices

* Reuse client instances in long-running services.
* Set explicit timeout budgets with `TimeoutConfig`.
* Log `result.raw` and `event.raw`, which carry the `request_id`.
* Retry only transport failures and idempotent requests.
* Keep sync and async execution models separate.

## Next steps

<Columns cols={2}>
  <Card title="Store the key safely" icon="key-round" href="/documentation/manage-account/api-key#store-it-safely">
    Keep it in a secret manager and rotate it if it leaks.
  </Card>

  <Card title="Use Parrot through TrueFoundry" icon="plug" href="/documentation/ringg-labs/parrot-stt/truefoundry">
    Gateway access control and tracing in front of Parrot.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.