> ## Documentation Index
> Fetch the complete documentation index at: https://hanabiaiinc-agents-response-wait-settings.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Speech to Text

> Transcribe audio with transcribe-1 or transcribe-1-pro, including multi-speaker conversations and emotion cues

Turn spoken audio into text using Fish Audio's automatic speech recognition (ASR) models. Send an audio file to `POST /v1/asr` to receive a transcript, its duration, and optional timestamped segments. Use `transcribe-1-pro` for multi-speaker conversations and transcripts that preserve emotion and vocal-event cues.

<CardGroup cols={3}>
  <Card title="Use it in the web app" icon="browser" href="https://fish.audio/app/speech-to-text">
    No code: upload audio, get a transcript.
  </Card>

  <Card title="API reference" icon="brackets-curly" href="/api-reference/endpoint/openapi-v1/speech-to-text">
    Every parameter for `POST /v1/asr`.
  </Card>

  <Card title="Cookbooks" icon="book-open" href="/developer-guide/sdk-guide/cookbook/transcribe-to-captions">
    Captions, batch transcription, and more.
  </Card>
</CardGroup>

## Choose an ASR model

| Model              | Use it for                                                                                  |
| ------------------ | ------------------------------------------------------------------------------------------- |
| `transcribe-1`     | General audio transcription. This is the default when the `model` header is omitted.        |
| `transcribe-1-pro` | Multi-speaker conversations, with emotion and vocal-event cues preserved in the transcript. |

Select the model with the **`model` HTTP header**. Both models use the same endpoint and request fields; `model` is not a form field. See [ASR pricing](/developer-guide/models-pricing/pricing-and-rate-limits#automatic-speech-recognition-asr-models) for usage costs.

### Multi-speaker conversations

`transcribe-1-pro` supports recordings with multiple speakers, such as interviews, meetings, and calls. Send the recording as one audio file to transcribe the conversation.

Pro identifies speakers with **inline `<|speaker:N|>` markers in the response's `text` string**, where `N` is a numeric speaker label. A marker assigns the following text to that speaker until the next marker. The same label can appear again when that speaker resumes talking.

For example, this illustrative response contains three turns from two speakers. It uses `ignore_timestamps=true`, so `segments` is empty:

```json theme={null}
{
  "text": "<|speaker:0|>你好。<|speaker:1|>[高兴]很开心认识你。<|speaker:0|>我也是。",
  "duration": 6.4,
  "segments": [],
  "language_code": "zh",
  "language": "Chinese"
}
```

| Marker            | How to read this example                                       |
| ----------------- | -------------------------------------------------------------- |
| `<\|speaker:0\|>` | Speaker 0 says `你好。`, then later `我也是。`.                       |
| `<\|speaker:1\|>` | Speaker 1 says `很开心认识你。`, with the emotion cue `[高兴]` (happy). |

Treat speaker labels as identifiers within that recording, not as names or identities you can match across separate requests. The response contains one annotated `text` string, not a JSON array of speaker turns. To display turns separately, split the string at speaker markers, preserving any emotion cues in each turn. For example, after decoding the JSON response into `result`:

```python theme={null}
import re

pattern = r"<\|speaker:(\d+)\|>(.*?)(?=<\|speaker:\d+\|>|$)"
for turn in re.finditer(pattern, result["text"], re.DOTALL):
    print(f"Speaker {turn.group(1)}: {turn.group(2).strip()}")
```

```text theme={null}
Speaker 0: 你好。
Speaker 1: [高兴]很开心认识你。
Speaker 0: 我也是。
```

With `ignore_timestamps=false`, `text` keeps the speaker markers, while `segments` contains aligned speech with `text`, `start`, and `end`. Speaker markers and emotion cues are excluded from timestamp alignment. There is no separate speaker list or per-segment `speaker_id`, so use the markers in `text` to read speaker turns; do not assume each segment corresponds to a turn.

### Emotion and vocal-event cues

Pro can retain cues about how speech sounds alongside the spoken words. These appear as inline bracketed text, such as `[高兴]` (happy) or `[laughter]`, in the response's `text` field. For example, a transcript might contain:

```text theme={null}
你好，[高兴]很开心认识你
```

These are annotations inferred from the audio, not words you need to add to the request. The API has no separate `emotion` field or fixed emotion enum; preserve the returned text when your application needs these cues.

Timestamp alignment uses the spoken words with speaker, emotion, and event markers removed. Read `text` for the annotated transcript and `segments` for timed speech; the segment text may therefore differ from the full transcript.

## When to use it

<CardGroup cols={2}>
  <Card title="Captions & subtitles" icon="closed-captioning">
    Timed segments map straight to SRT/VTT cues.
  </Card>

  <Card title="Meeting & call notes" icon="users">
    Use Pro to transcribe multi-speaker recordings for summaries and search.
  </Card>

  <Card title="Voice commands & notes" icon="microphone-lines">
    Turn short utterances into text your app can act on.
  </Card>

  <Card title="Accessibility" icon="universal-access">
    Make audio and video content readable.
  </Card>
</CardGroup>

## Quick start

Create an [API key](/developer-guide/getting-started/api-key) and set `FISH_API_KEY` in your environment. For the Python examples, [install the Fish Audio SDK](/developer-guide/sdk-guide/quickstart).

The examples below select `transcribe-1-pro` and request timestamps. Set the header to `transcribe-1` to use the standard model.

<CodeGroup>
  ```python Python theme={null}
  from fishaudio import FishAudio
  from fishaudio.core import RequestOptions

  client = FishAudio()  # reads FISH_API_KEY

  with open("speech.wav", "rb") as f:
      result = client.asr.transcribe(
          audio=f.read(),
          include_timestamps=True,
          request_options=RequestOptions(
              additional_headers={"model": "transcribe-1-pro"}
          ),
      )

  print(result.text)

  ```

  ```bash API (curl) theme={null}
  curl --request POST https://api.fish.audio/v1/asr \
    --header "Authorization: Bearer $FISH_API_KEY" \
    --header "model: transcribe-1-pro" \
    --form audio=@speech.wav \
    --form ignore_timestamps=false
  ```

  ```javascript JavaScript (Node.js 20+) theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append("audio", new Blob([await readFile("speech.wav")]), "speech.wav");
  form.append("ignore_timestamps", "false");

  const response = await fetch("https://api.fish.audio/v1/asr", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FISH_API_KEY}`,
      model: "transcribe-1-pro",
    },
    body: form,
  });

  if (!response.ok) throw new Error(await response.text());
  const result = await response.json();
  console.log(result.text);
  ```
</CodeGroup>

The response gives you the full `text`, the audio `duration` in seconds, and `segments`. When available, it also includes the detected `language_code` and display name `language`.

<Note>
  The Python SDK selects Pro through `RequestOptions.additional_headers`;
  `asr.transcribe()` has no `model` argument. The JavaScript example calls the
  REST API directly so the header is explicit. For multipart uploads, let your
  HTTP client set `Content-Type` and its boundary.
</Note>

## Read the timestamps

Each segment carries `text`, `start`, and `end`, with times in **seconds**. With the API, request timestamps with `ignore_timestamps=false`. The default is `true`, which returns an empty `segments` array. Alignment adds processing time, and `segments` can also be empty when alignment is unavailable or no speech is detected.

<CodeGroup>
  ```python Python theme={null}
  result = client.asr.transcribe(
      audio=audio_bytes,
      include_timestamps=True,
      request_options=RequestOptions(
          additional_headers={"model": "transcribe-1-pro"}
      ),
  )

  print(f"{result.duration:.1f}s total")
  for seg in result.segments:
      print(f"[{seg.start:6.2f} - {seg.end:6.2f}] {seg.text}")

  ```

  ```bash API (curl) theme={null}
  curl --request POST https://api.fish.audio/v1/asr \
    --header "Authorization: Bearer $FISH_API_KEY" \
    --header "model: transcribe-1-pro" \
    --form audio=@speech.wav \
    --form ignore_timestamps=false | jq '.segments'

  # Each segment: { "text": "One", "start": 0.0, "end": 0.24 }
  ```
</CodeGroup>

<Note>
  In the Python SDK, segment timestamps are **on by default**. Pass
  `include_timestamps=False` to skip them. That's the *inverse* of the
  API/JavaScript flag `ignore_timestamps`.
</Note>

## Implementation details

### Language

`language` is an optional hint, such as `en`, `zh`, or `ja`. Language detection still runs when you provide a hint; it does not force the returned language. Use the response's `language_code` in application logic and `language` for display when those fields are available.

<CodeGroup>
  ```python Python theme={null}
  # Auto-detect
  result = client.asr.transcribe(audio=audio_bytes)

  # Provide a language hint
  result = client.asr.transcribe(audio=audio_bytes, language="zh")

  ```

  ```bash API (curl) theme={null}
  # Omit the form field to auto-detect, or set it explicitly:
  curl --request POST https://api.fish.audio/v1/asr \
    --header "Authorization: Bearer $FISH_API_KEY" \
    --form audio=@speech.wav \
    --form language=zh
  ```
</CodeGroup>

### Input audio

Common formats work directly: `wav`, `mp3`, `opus`, and more. Send the raw file bytes; no pre-processing required. The endpoint accepts `multipart/form-data` (shown above) or `application/msgpack`.

### Long recordings

One request transcribes one audio file. For long recordings, split the audio into shorter clips and transcribe each, then offset each chunk's `start`/`end` by where it began in the full recording. Check that each response covers the complete clip before combining transcripts.

### Async transcription

The Python SDK ships an async client with the same surface, useful when you're transcribing many files concurrently or already running inside an event loop. Use `AsyncFishAudio` and `await` the call:

```python theme={null}
import asyncio
from fishaudio import AsyncFishAudio
from fishaudio.core import RequestOptions

async def main():
    client = AsyncFishAudio()  # reads FISH_API_KEY
    with open("speech.wav", "rb") as f:
        result = await client.asr.transcribe(
            audio=f.read(),
            request_options=RequestOptions(
                additional_headers={"model": "transcribe-1-pro"}
            ),
        )
    print(result.text)

asyncio.run(main())
```

To run several files in parallel, gather the coroutines:

```python theme={null}
import asyncio
from fishaudio import AsyncFishAudio

async def transcribe_all(paths):
    client = AsyncFishAudio()
    clips = [open(p, "rb").read() for p in paths]
    return await asyncio.gather(*[
        client.asr.transcribe(audio=clip, language="en") for clip in clips
    ])

for result in asyncio.run(transcribe_all(["speech.wav"])):
    print(result.text)
```

### Direct API (MessagePack)

`POST /v1/asr` also accepts a [MessagePack](https://msgpack.org) body instead of multipart form data, the same path the API reference links to for low-overhead, server-side calls. Pack the audio bytes and options into one payload and set `Content-Type: application/msgpack`:

```python theme={null}
import os
import httpx
import ormsgpack

with open("speech.wav", "rb") as f:
    audio = f.read()

payload = {"audio": audio, "language": "en", "ignore_timestamps": False}

resp = httpx.post(
    "https://api.fish.audio/v1/asr",
    content=ormsgpack.packb(payload),
    headers={
        "Authorization": f"Bearer {os.environ['FISH_API_KEY']}",
        "Content-Type": "application/msgpack",
        "model": "transcribe-1-pro",
    },
)
resp.raise_for_status()
result = resp.json()
print(result["text"])
```

The response shape is identical to the multipart path: `text`, `duration` (seconds), `segments`, and optional detected-language fields. Model selection stays in the HTTP header for both formats.

## Going further

<CardGroup cols={2}>
  <Card title="Generate speech" icon="microphone" href="/features/text-to-speech">
    The reverse direction: text to lifelike audio.
  </Card>

  <Card title="Full API parameters" icon="book-open" href="/api-reference/endpoint/openapi-v1/speech-to-text">
    Every field and the raw response schema.
  </Card>

  <Card title="Python reference" icon="python" href="/api-reference/sdk/python/resources">
    `asr.transcribe` options and the `ASRResponse` type.
  </Card>
</CardGroup>
