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

# Podcast Transcription with Speaker Labels

> Transcribe multi-speaker audio at scale using the Gnani Prisma v2.5 Batch STT API. From submitting an audio file to receiving a clean, speaker-separated transcript — in 10 Indian languages.

## Overview

Transcribe multi-speaker audio at scale using the Gnani Prisma v2.5 Batch STT API. This guide walks through every step — from submitting an audio file to receiving a clean, speaker-separated transcript — using podcast transcription as the working example.

Audio-first content — podcasts, interview recordings, panel discussions — carries information that stays locked unless it is transcribed. Speaker-level transcription is what separates a readable document from a wall of undifferentiated text. You know who said what, when they said it, and for how long.

| Capability                   | What it enables downstream                                                                                                                          |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Speaker-separated output** | Per-speaker text blocks mean editors can review one voice at a time, and content teams can attribute quotes accurately.                             |
| **Time-aligned segments**    | Every segment carries a `start_time` and `end_time`, enabling subtitle generation, chapter markers, and clip extraction at a specific timestamp.    |
| **9 Indian languages**       | Transcribe Hindi, Tamil, Telugu, Kannada, Malayalam, Bengali (India and Bangladesh), Marathi, and English without switching providers or pipelines. |
| **Batch processing**         | Submit up to 100 files in a single job. Run overnight jobs, backfill archives, or process weekly episode batches without managing queues yourself.  |

***

## Other Use Cases

The same create-start-poll-download pipeline works for any long-form, speaker-rich audio. Any scenario involving long audio files, two speakers, and a need for speaker-separated text maps directly to this pipeline.

| Use Case                                  | Description                                                                                                                                              |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Journalist Interviews**                 | Transcribe field recordings with interviewer and subject separated. Feed directly into editorial workflows without manual formatting.                    |
| **Parliamentary & Panel Debates**         | Attribute statements to the correct speaker for political reporting, fact-checking, or archival. Supports Devanagari and regional scripts natively.      |
| **EdTech Lecture Recordings**             | Transcribe faculty and student exchanges. Generate accessible transcripts for students, search indexes for course platforms, and study material exports. |
| **Legal Depositions & Hearings**          | Produce verbatim speaker-attributed records of proceedings for court reporter review and archival.                                                       |
| **Radio Archive Digitisation**            | Backfill years of archived broadcasts into searchable, attributed text. Batch processing handles large volumes without manual queuing.                   |
| **Corporate Town Halls & Earnings Calls** | Generate attributed transcripts of leadership Q\&A sessions. Surface speaker-specific statements for internal comms or investor relations.               |
| **Documentary & Film Production**         | Auto-generate interview transcripts for rough-cut editing. Export time-coded speaker lines directly to editing software.                                 |
| **Doctor-Patient Consultations**          | Transcribe recorded consultations with doctor and patient separated. Enable structured documentation workflows for EMR systems.                          |

<Note>
  **Two-speaker limit:** The Gnani Prisma v2.5 Batch STT API supports a maximum of two distinct speakers per file. It is optimised for two-party audio — interviews, conversations, and one-on-one recordings. Panel discussions with three or more speakers are outside the current scope.
</Note>

***

## Prerequisites

| Requirement                   | Details                                                                                                                                      |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Gnani Prisma v2.5 API key** | Available from the Gnani API dashboard. You will use this as the `X-API-Key-ID` header on every request.                                     |
| **Python 3.10+**              | The pipeline uses f-strings, `pathlib`, and `typing` patterns that require Python 3.10 or later.                                             |
| **Audio files**               | Supported formats: WAV, MP3, MP4, FLAC, OGG, Opus, M4A, AAC, WebM, AMR. Each file must be **10 MB or smaller**. Up to **100 files** per job. |

```bash theme={null}
# HTTP client (used for create, start, poll, and download calls)
pip install requests
```

No SDK is required for this pipeline. All calls use the standard HTTP REST endpoints.

***

## Authentication

| Header             | Required | Description                                                                                        |
| ------------------ | -------- | -------------------------------------------------------------------------------------------------- |
| `X-API-Key-ID`     | Yes      | Your Gnani Prisma v2.5 API key. Required on every Batch STT request.                               |
| `X-API-Request-ID` | No       | A UUID you assign for tracing. Useful for correlating your application logs with platform support. |

Store your API key as an environment variable. Never hardcode it in source files or commit it to version control.

```bash .env theme={null}
GNANI_API_KEY=your-api-key-here
```

```python loading credentials theme={null}
import os

API_KEY = os.getenv("GNANI_API_KEY")
HEADERS = {"X-API-Key-ID": API_KEY}
```

<Warning>
  **Never hardcode API keys.** Do not commit credentials to version control. Use environment variables, a secrets manager, or a vault. Rotate your key immediately if it is exposed.
</Warning>

***

## Limits & Supported Formats

| Item                      | Limit                                                                        |
| ------------------------- | ---------------------------------------------------------------------------- |
| Max file size             | **10 MB** per file                                                           |
| Max files per job         | **100** files                                                                |
| Max ZIP compressed size   | **50 MB**                                                                    |
| Max ZIP decompressed size | **200 MB**                                                                   |
| Minimum poll interval     | **10 seconds** between status calls for the same `job_id`                    |
| Speaker diarization       | Maximum **2** speakers per file                                              |
| `transcript_url` expiry   | **1 hour** — re-call [Get Job Files](/api/STTBatch/Get_Job_Files) if expired |

See [Batch STT Introduction — Limits](/api/STTBatch/Introduction#limits) for the full reference.

### Supported Audio Formats

`.wav` · `.mp3` · `.mp4` · `.flac` · `.ogg` · `.opus` · `.m4a` · `.aac` · `.webm` · `.amr`

| Format         | Extension       | Notes                                                                      |
| -------------- | --------------- | -------------------------------------------------------------------------- |
| **WAV**        | `.wav`          | Uncompressed. Highest quality but largest file size.                       |
| **FLAC**       | `.flac`         | Lossless compression. Good balance of quality and size for archival audio. |
| **AAC / M4A**  | `.m4a`, `.aac`  | Common podcast export format. Well-supported across recording tools.       |
| **MP3**        | `.mp3`          | Widely used lossy format.                                                  |
| **OGG / Opus** | `.ogg`, `.opus` | Efficient compression — recommended for high-volume batch jobs.            |
| **WebM / AMR** | `.webm`, `.amr` | Supported for mobile and web-captured audio.                               |

<Warning>
  **Files over 10 MB:** Compress or re-encode before upload, or split into smaller segments and stitch transcripts in order after download. The Batch API does not accept individual files larger than 10 MB.
</Warning>

***

## Supported Languages

Pass the BCP-47 code in the `language_code` field of your job `config`.

| Language                 | Code    | Native Script |
| ------------------------ | ------- | ------------- |
| **Bengali (Bangladesh)** | `bn-BD` | বাংলা         |
| **Bengali (India)**      | `bn-IN` | বাংলা         |
| **English (India)**      | `en-IN` | Latin         |
| **Hindi**                | `hi-IN` | हिन्दी        |
| **Kannada**              | `kn-IN` | ಕನ್ನಡ         |
| **Malayalam**            | `ml-IN` | മലയാളം        |
| **Marathi**              | `mr-IN` | मराठी         |
| **Tamil**                | `ta-IN` | தமிழ்         |
| **Telugu**               | `te-IN` | తెలుగు        |

<Info>
  **ITN not supported on Batch STT.** Inverse Text Normalization (spoken numbers, currency, dates) is not available in the Batch Jobs API. Use [REST STT](/api/STT/speech-to-text) if you need ITN on short clips.
</Info>

***

## Pipeline

Creating a job does **not** start transcription. You must call `/start` after upload.

| Step          | Endpoint                                                 | Reference                                    |
| ------------- | -------------------------------------------------------- | -------------------------------------------- |
| 1. Create     | `POST /stt/v3/batch/jobs`                                | [Create Job](/api/STTBatch/Create_Job)       |
| 2. Start      | `POST /stt/v3/batch/jobs/{job_id}/start`                 | [Start Job](/api/STTBatch/Start_Job)         |
| 3. Poll       | `GET /stt/v3/batch/jobs/{job_id}`                        | [Get Job Status](/api/STTBatch/Get_Job)      |
| 4. List files | `GET /stt/v3/batch/jobs/{job_id}/files?status=COMPLETED` | [Get Job Files](/api/STTBatch/Get_Job_Files) |
| 5. Download   | `GET <transcript_url>`                                   | JSON with `full_transcript` + `segments`     |

<Steps>
  <Step title="Create the job">
    POST to `/stt/v3/batch/jobs` with a multipart body: a `config` JSON field and one or more `files`. Receive a `job_id` with status `CREATED`. Transcription has not started yet.
  </Step>

  <Step title="Start the job">
    POST to `/stt/v3/batch/jobs/{job_id}/start`. Status moves to `STARTING`, then `QUEUED` → `IN_PROGRESS` → `COMPLETED`.
  </Step>

  <Step title="Poll for completion">
    GET `/stt/v3/batch/jobs/{job_id}` every **10 seconds** until status reaches a terminal state (`COMPLETED`, `PARTIAL_FAILURE`, `FAILED`, or `CANCELLED`).
  </Step>

  <Step title="Fetch transcript URLs">
    GET `/stt/v3/batch/jobs/{job_id}/files?status=COMPLETED`. Each completed file includes a `transcript_url` (valid for 1 hour).
  </Step>

  <Step title="Download and parse">
    GET each `transcript_url` to retrieve JSON with `full_transcript` and `segments`. Group by `speaker_id`, build per-speaker text blocks with timestamps, and save output files.
  </Step>
</Steps>

### Step 1 — Create Job

```python create_job() theme={null}
import json
import os
import requests
from pathlib import Path

BATCH_JOBS = "https://api.vachana.ai/stt/v3/batch/jobs"

def create_job(
    audio_paths: list[str],
    language_code: str = "hi-IN",
    with_diarization: bool = True,
    num_speakers: int = 2,
) -> str:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}

    config = {
        "model": "gnani-prisma-v2.5",
        "language_code": language_code,
        "mode": "transcribe",
        "with_diarization": with_diarization,
        "is_multi_channel": False,
    }
    if with_diarization:
        config["num_speakers"] = num_speakers

    files = [
        ("config", (None, json.dumps(config), "application/json")),
    ]
    for path in audio_paths:
        files.append(("files", (Path(path).name, open(path, "rb"), "audio/wav")))

    resp = requests.post(BATCH_JOBS, headers=headers, files=files)

    for key, (_, fh, _) in files:
        if key == "files":
            fh.close()

    resp.raise_for_status()
    job_id = resp.json()["job_id"]
    print(f"Created job. job_id: {job_id}")
    return job_id
```

<Tip>
  **Multiple files:** Add additional `("files", ...)` tuples. Up to **100** files per job, each **10 MB** or smaller.
</Tip>

### Step 2 — Start Job

```python start_job() theme={null}
BATCH_START = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/start"

def start_job(job_id: str) -> None:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}
    url = BATCH_START.format(job_id=job_id)

    resp = requests.post(url, headers=headers)
    resp.raise_for_status()
    print(f"Start accepted. status: {resp.json()['status']}")
```

### Step 3 — Poll

```python poll_until_complete() theme={null}
import time
from typing import Optional

BATCH_STATUS  = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}"
POLL_INTERVAL = 10  # seconds — minimum recommended interval

TERMINAL_STATUSES = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"}

def poll_until_complete(job_id: str) -> Optional[str]:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}
    url     = BATCH_STATUS.format(job_id=job_id)

    print(f"Polling job {job_id} every {POLL_INTERVAL}s...")

    while True:
        time.sleep(POLL_INTERVAL)

        resp = requests.get(url, headers=headers)
        resp.raise_for_status()
        payload = resp.json()
        status  = payload["status"]

        completed = payload.get("completed_files", 0)
        total     = payload.get("total_files", "?")
        print(f"  [{status}]  {completed}/{total} files complete")

        if status in TERMINAL_STATUSES:
            if status == "COMPLETED":
                print(f"Job complete. {completed} file(s) transcribed.")
                return status
            print(f"Job ended with status: {status}")
            return status
```

<Warning>
  **Minimum poll interval: 10 seconds.** Do not poll more frequently than every 10 seconds for the same `job_id`.
</Warning>

### Step 4 — Fetch Transcript URLs

```python fetch_completed_files() theme={null}
BATCH_FILES = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/files"

def fetch_completed_files(job_id: str) -> list[dict]:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}
    url     = BATCH_FILES.format(job_id=job_id)

    resp = requests.get(url, headers=headers, params={"status": "COMPLETED"})
    resp.raise_for_status()
    return resp.json().get("files", [])
```

### Step 5 — Download & Parse

```python download_and_parse() theme={null}
import json
from pathlib import Path
from typing import Dict

def download_transcript(transcript_url: str) -> dict:
    resp = requests.get(transcript_url)
    resp.raise_for_status()
    return resp.json()

def parse_transcript(transcript: dict, original_path: str, output_dir: Path) -> dict:
    fname    = Path(original_path).stem
    segments = transcript.get("segments", [])

    if not segments:
        print(f"Skipping {fname}: no segments")
        return {}

    lines, speaker_times, segment_meta = [], {}, []

    for seg in segments:
        spk   = seg.get("speaker_id", "UNKNOWN")
        text  = seg.get("text", "").strip()
        start = seg.get("start_time", 0.0)
        end   = seg.get("end_time",   0.0)

        ts = f"{int(start // 60):02d}:{int(start % 60):02d}"
        lines.append(f"[{ts}] SPEAKER_{spk}: {text}")
        speaker_times[spk] = speaker_times.get(spk, 0.0) + (end - start)
        segment_meta.append({
            "segment_id":        seg.get("segment_id"),
            "speaker_id":        spk,
            "start_time":        start,
            "end_time":          end,
            "text":              text,
            "language_detected": seg.get("language_detected"),
        })

    transcript_path = output_dir / f"{fname}_transcript.txt"
    transcript_path.write_text("\n".join(lines), encoding="utf-8")

    metadata_path = output_dir / f"{fname}_metadata.json"
    metadata_path.write_text(json.dumps({
        "filename":          original_path,
        "full_transcript":   transcript.get("full_transcript"),
        "duration_seconds":  transcript.get("duration_seconds"),
        "speaker_talk_time": {f"SPEAKER_{k}": round(v, 2) for k, v in speaker_times.items()},
        "segments":          segment_meta,
    }, indent=2, ensure_ascii=False), encoding="utf-8")

    print(f"Parsed: {fname} → {len(lines)} segments, {len(speaker_times)} speaker(s)")
    return {
        "transcript_path": str(transcript_path),
        "metadata_path":   str(metadata_path),
    }
```

***

## Full Script

```python podcast_transcription.py theme={null}
import json
import os
import time
import requests
from pathlib import Path
from typing import Dict, List, Optional

BATCH_JOBS    = "https://api.vachana.ai/stt/v3/batch/jobs"
BATCH_START   = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/start"
BATCH_STATUS  = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}"
BATCH_FILES   = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/files"
POLL_INTERVAL = 10
OUTPUT_DIR    = "outputs"

TERMINAL_STATUSES = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"}

Path(OUTPUT_DIR).mkdir(exist_ok=True)


def create_job(
    audio_paths: List[str],
    language_code: str = "hi-IN",
    with_diarization: bool = True,
    num_speakers: int = 2,
) -> str:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}

    config = {
        "model": "gnani-prisma-v2.5",
        "language_code": language_code,
        "mode": "transcribe",
        "with_diarization": with_diarization,
        "is_multi_channel": False,
    }
    if with_diarization:
        config["num_speakers"] = num_speakers

    files = [("config", (None, json.dumps(config), "application/json"))]
    for path in audio_paths:
        files.append(("files", (Path(path).name, open(path, "rb"), "audio/wav")))

    resp = requests.post(BATCH_JOBS, headers=headers, files=files)
    for key, (_, fh, _) in files:
        if key == "files":
            fh.close()

    resp.raise_for_status()
    job_id = resp.json()["job_id"]
    print(f"Created {len(audio_paths)} file(s). job_id: {job_id}")
    return job_id


def start_job(job_id: str) -> None:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}
    resp = requests.post(BATCH_START.format(job_id=job_id), headers=headers)
    resp.raise_for_status()
    print(f"Start accepted. status: {resp.json()['status']}")


def poll_until_complete(job_id: str) -> Optional[str]:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}
    url     = BATCH_STATUS.format(job_id=job_id)

    print(f"Polling every {POLL_INTERVAL}s...")
    while True:
        time.sleep(POLL_INTERVAL)
        resp    = requests.get(url, headers=headers)
        resp.raise_for_status()
        payload = resp.json()
        status  = payload["status"]
        print(f"  [{status}]  {payload.get('completed_files', 0)}/{payload.get('total_files', '?')} files")
        if status in TERMINAL_STATUSES:
            return status if status == "COMPLETED" else None


def fetch_completed_files(job_id: str) -> list:
    api_key = os.getenv("GNANI_API_KEY")
    headers = {"X-API-Key-ID": api_key}
    resp = requests.get(
        BATCH_FILES.format(job_id=job_id),
        headers=headers,
        params={"status": "COMPLETED"},
    )
    resp.raise_for_status()
    return resp.json().get("files", [])


def parse_transcript(transcript: dict, original_path: str, output_dir: Path) -> Dict[str, str]:
    fname    = Path(original_path).stem
    segments = transcript.get("segments", [])
    if not segments:
        return {}

    lines, speaker_times, segment_meta = [], {}, []
    for seg in segments:
        spk   = seg.get("speaker_id", "UNKNOWN")
        text  = seg.get("text", "").strip()
        start = seg.get("start_time", 0.0)
        end   = seg.get("end_time",   0.0)
        ts    = f"{int(start // 60):02d}:{int(start % 60):02d}"
        lines.append(f"[{ts}] SPEAKER_{spk}: {text}")
        speaker_times[spk] = speaker_times.get(spk, 0.0) + (end - start)
        segment_meta.append({
            "segment_id": seg.get("segment_id"),
            "speaker_id": spk,
            "start_time": start,
            "end_time":   end,
            "text":       text,
        })

    transcript_path = output_dir / f"{fname}_transcript.txt"
    transcript_path.write_text("\n".join(lines), encoding="utf-8")

    metadata_path = output_dir / f"{fname}_metadata.json"
    metadata_path.write_text(json.dumps({
        "filename":          original_path,
        "full_transcript":   transcript.get("full_transcript"),
        "duration_seconds":  transcript.get("duration_seconds"),
        "speaker_talk_time": {f"SPEAKER_{k}": round(v, 2) for k, v in speaker_times.items()},
        "segments":          segment_meta,
    }, indent=2, ensure_ascii=False), encoding="utf-8")

    print(f"Saved: {transcript_path.name}")
    return {"transcript_path": str(transcript_path), "metadata_path": str(metadata_path)}


if __name__ == "__main__":
    job_id = create_job(
        audio_paths=["/path/to/episode_01.wav"],
        language_code="hi-IN",
        with_diarization=True,
        num_speakers=2,
    )
    start_job(job_id)

    if poll_until_complete(job_id):
        output_dir = Path(OUTPUT_DIR) / f"job_{job_id}"
        output_dir.mkdir(parents=True, exist_ok=True)

        outputs = {}
        for file_entry in fetch_completed_files(job_id):
            transcript_url = file_entry.get("transcript_url")
            if not transcript_url:
                continue
            transcript = requests.get(transcript_url).json()
            result = parse_transcript(transcript, file_entry["original_path"], output_dir)
            if result:
                outputs[Path(file_entry["original_path"]).stem] = result

        print(f"\nDone. {len(outputs)} transcript(s) saved to {output_dir}/")
```

***

## Sample Output

```text theme={null}
outputs/
└── job_019fa79e-81f9-7a8a-a446-6eff16ddec30/
    ├── episode_01_transcript.txt   ← speaker-labelled, time-stamped transcript
    └── episode_01_metadata.json    ← full_transcript, talk time, segment detail
```

**episode\_01\_transcript.txt**

```text theme={null}
[00:00] SPEAKER_1: नमस्ते, मैं हूँ रवि शर्मा और आज हम बात करेंगे भारत के स्टार्टअप इकोसिस्टम के बारे में।
[00:07] SPEAKER_2: हाँ रवि जी, बहुत अच्छा विषय है। पिछले पाँच साल में बहुत कुछ बदला है।
[00:14] SPEAKER_1: बिल्कुल। दो लाख करोड़ से ज़्यादा की फंडिंग आई है 2024 में।
[00:22] SPEAKER_2: और यूनिकॉर्न्स की संख्या भी 100 के पार पहुँच गई है।
```

**episode\_01\_metadata.json**

```json theme={null}
{
  "filename": "episode_01.wav",
  "full_transcript": "नमस्ते, मैं हूँ रवि शर्मा...",
  "duration_seconds": 2847.5,
  "speaker_talk_time": {
    "SPEAKER_1": 1423.8,
    "SPEAKER_2": 1389.2
  },
  "segments": [
    {
      "segment_id": 0,
      "speaker_id": 1,
      "start_time": 0.0,
      "end_time": 6.8,
      "text": "नमस्ते, मैं हूँ रवि शर्मा..."
    }
  ]
}
```

***

<Info>
  **Related docs:** [Batch STT Introduction](/api/STTBatch/Introduction) · [Create Job](/api/STTBatch/Create_Job) · [Start Job](/api/STTBatch/Start_Job) · [Get Job Status](/api/STTBatch/Get_Job) · [Get Job Files](/api/STTBatch/Get_Job_Files)
</Info>
