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

# Transcribe your first recording

> Stream a recorded call to Rime speech-to-text with the SDK in Python, Node.js, Go, or Rust, and print its partial and final transcripts. The quickstart also transcribes a whole file in one call.

Stream a recorded call to Rime speech-to-text, and your terminal prints its transcript. Partial transcripts appear while the call streams, and a final transcript follows that you can compare with the recording. A one-line change then transcribes the same call word for word. Run the script on your computer or on a server, not in a browser.

**1. Install the SDK**

The SDK packages are in alpha, so these commands pin the exact versions that this page was tested with. The [rime-sdk repository](https://github.com/rimelabs/rime-sdk) on GitHub has the guides, examples, and options for each language.

**Python**

Install [uv](https://docs.astral.sh/uv/getting-started/installation/). Then create a project and add the SDK:

```bash theme={null}
uv init --python 3.12 rime-stt-quickstart
cd rime-stt-quickstart
uv add "rimelabs-sdk==0.1.0a17"
```

**Node.js**

Use Node.js 22 or later. Create a project and add the SDK:

```bash theme={null}
mkdir rime-stt-quickstart
cd rime-stt-quickstart
npm init -y
npm install --save-exact @rimelabs/sdk@0.1.0-alpha.17
```

**Go**

Use Go 1.24 or later. Create a module and add the SDK:

```bash theme={null}
mkdir rime-stt-quickstart
cd rime-stt-quickstart
go mod init example.com/rime-stt-quickstart
go get github.com/rimelabs/rime-sdk/go@v0.1.0-alpha.9
```

**Rust**

Use Rust 1.88 or later. Create a project. Add the SDK and the crates that the script uses to read and stream the file:

```bash theme={null}
cargo new rime-stt-quickstart
cd rime-stt-quickstart
cargo add rimelabs-sdk@=0.1.0-alpha.7 bytes futures-util
cargo add tokio --features macros,rt-multi-thread,net,fs,io-util
```

The commands add these dependencies to `Cargo.toml`. When you run the commands, the `bytes`, `futures-util`, and `tokio` versions can be newer:

```toml Cargo.toml theme={null}
[dependencies]
bytes = "1.12.1"
futures-util = "0.3.34"
rimelabs-sdk = "=0.1.0-alpha.7"
tokio = { version = "1.54.0", features = ["macros", "rt-multi-thread", "net", "fs", "io-util"] }
```

**2. Set your API key**

The SDK reads your key from `RIME_API_KEY` when it creates the client, and it does not read `.env` files. If you need a key, [create one](/docs/api-authentication). Then set the variable in the terminal where you will run the script:

**macOS and Linux**

```bash theme={null}
export RIME_API_KEY="YOUR_API_KEY"
```

**Windows PowerShell**

```powershell theme={null}
$env:RIME_API_KEY = "YOUR_API_KEY"
```

**3. Download the sample recording**

This command saves a 9-second recording of a caller to your project folder. Rime text-to-speech made the recording (Coda, voice hesse). The caller says "Uh, hi. I'm calling about flight two four seven to Denver. Can I, um, move it to Friday?"

**macOS and Linux**

```bash theme={null}
curl -fsSLO https://docs.rime.ai/audio/stt/flight.wav && tail -c +45 flight.wav > flight.pcm
```

**Windows PowerShell**

```powershell theme={null}
curl.exe -fsSLO https://docs.rime.ai/audio/stt/flight.wav
$wav = [IO.File]::ReadAllBytes("$PWD\flight.wav")
[IO.File]::WriteAllBytes("$PWD\flight.pcm", [byte[]]$wav[44..($wav.Length - 1)])
```

The command downloads `flight.wav`. It removes the 44-byte WAV header and saves the audio as `flight.pcm`. This file has raw 16-bit samples at 16 kHz, mono, which is the default input format of the SDK.

To listen to the recording, open [flight.wav](/audio/stt/flight.wav) in your browser. To use your own WAV or MP3 file, convert it first, as [Use your own recording](#use-your-own-recording) shows.

**4. Save the script**

This script sends the recording to Rime in 100-millisecond chunks, as fast as it reads the file. The script prints every update that the SDK returns. A live microphone or call delivers audio in real time, so pass that audio to the SDK as it arrives.

Save the script in your project folder.

**Python**

```python transcribe.py theme={null}
import asyncio
import sys

from rimelabs_sdk import Rime, TranscriptionMode

CHUNK_BYTES = 3200  # 100 ms of 16 kHz, 16-bit mono audio


async def audio_chunks(path):
    with open(path, "rb") as audio:
        while chunk := await asyncio.to_thread(audio.read, CHUNK_BYTES):
            yield chunk


async def main(path):
    async with Rime() as client:
        async with client.stt.stream(
            audio_chunks(path),
            language="en",
            mode=TranscriptionMode.WRITTEN,
        ) as transcript:
            async for update in transcript:
                print(f"{update.kind}: {update.text}")
            print(f"Request: {transcript.request_id}")


asyncio.run(main(sys.argv[1]))
```

**Node.js**

```javascript transcribe.mjs theme={null}
import { createReadStream } from "node:fs";
import { Rime } from "@rimelabs/sdk";

const CHUNK_BYTES = 3200; // 100 ms of 16 kHz, 16-bit mono audio

const client = new Rime();
try {
  const audio = createReadStream(process.argv[2], { highWaterMark: CHUNK_BYTES });
  const transcript = client.stt.stream(audio, {
    language: "en",
    mode: "written",
  });
  for await (const update of transcript) {
    console.log(`${update.kind}: ${update.text}`);
  }
  console.log(`Request: ${transcript.requestId}`);
} finally {
  await client.close();
}
```

**Go**

```go main.go theme={null}
package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "os"

    rime "github.com/rimelabs/rime-sdk/go"
)

const chunkBytes = 3200 // 100 ms of 16 kHz, 16-bit mono audio

func main() {
    if err := transcribe(os.Args[1]); err != nil {
        log.Fatal(err)
    }
}

func transcribe(path string) error {
    audio, err := os.Open(path)
    if err != nil {
        return err
    }
    defer audio.Close()

    chunk := make([]byte, chunkBytes)
    audioChunks := func(ctx context.Context) ([]byte, error) {
        if err := ctx.Err(); err != nil {
            return nil, err
        }
        n, err := audio.Read(chunk)
        if err != nil {
            return nil, err // io.EOF when the file runs out
        }
        return chunk[:n], nil
    }

    client, err := rime.NewClient(rime.Config{})
    if err != nil {
        return err
    }
    defer client.Close()

    transcript, err := client.STT.Stream(context.Background(), audioChunks, rime.TranscriptionOptions{
        Language: "en",
        Mode:     rime.TranscriptionWritten,
    })
    if err != nil {
        return err
    }
    defer transcript.Close()

    for {
        update, err := transcript.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        switch update := update.(type) {
        case rime.TranscriptionPartial:
            fmt.Println("partial:", update.Text)
        case rime.TranscriptionFinal:
            fmt.Println("final:", update.Text)
        }
    }
    fmt.Println("Request:", transcript.RequestID())
    return nil
}
```

**Rust**

Cargo created `src/main.rs`. Replace its contents with this script:

```rust src/main.rs theme={null}
use bytes::Bytes;
use futures_util::{StreamExt, stream};
use rimelabs_sdk::{Client, Error, TranscriptionMode, TranscriptionOptions, TranscriptionUpdate};
use tokio::{fs::File, io::AsyncReadExt};

const CHUNK_BYTES: usize = 3200; // 100 ms of 16 kHz, 16-bit mono audio

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).ok_or("missing file name")?;
    let audio = File::open(path).await?;
    let audio_chunks = stream::try_unfold(audio, |mut audio| async move {
        let mut chunk = vec![0; CHUNK_BYTES];
        let n = audio.read(&mut chunk).await.map_err(Error::input)?;
        if n == 0 {
            return Ok(None); // the file ran out
        }
        chunk.truncate(n);
        Ok(Some((Bytes::from(chunk), audio)))
    });

    let client = Client::builder().build()?;
    let options = TranscriptionOptions::new("en").mode(TranscriptionMode::Written);
    let mut transcript = client.stt().stream(audio_chunks, options)?;
    while let Some(update) = transcript.next().await {
        match update? {
            TranscriptionUpdate::Partial { text } => println!("partial: {text}"),
            TranscriptionUpdate::Final { text, .. } => println!("final: {text}"),
        }
    }
    println!("Request: {}", transcript.request_id().unwrap_or_default());
    client.close().await;
    Ok(())
}
```

The SDK sends audio and reads updates at the same time. At the end of the file, the SDK tells Rime that the utterance is over. The loop ends when the final transcript arrives.

**5. Run it**

Run the script with the file name of the recording:

**Python**

```bash theme={null}
uv run transcribe.py flight.pcm
```

**Node.js**

```bash theme={null}
node transcribe.mjs flight.pcm
```

**Go**

```bash theme={null}
go run . flight.pcm
```

The first run takes longer, because it also compiles the SDK.

**Rust**

```bash theme={null}
cargo run -- flight.pcm
```

The first run takes longer, because it also compiles the SDK. Cargo prints its build lines before the output of the script.

The partial lines appear first, and the final line follows. This is the output from one run:

```text theme={null}
partial: Hi. I'm calling
partial: Hi! I'm calling about flight 247 to Denver.
partial: Hi! I'm calling about flight 247 to Denver. Can I, move it to Friday?
final: Hi! I'm calling about flight 247 to Denver. Can I, move it to Friday?
Request: 0a6548f0-8686-4942-9dc1-e435635bd725
```

The output has three kinds of lines:

* `partial`: the whole transcript at that time. Each `partial` line replaces the line before it. Do not write code that depends on the number of partials or on the text of each partial.
* `final`: the complete transcript. If the script raises an error and prints no final line, find the error in [Errors and limits](/docs/stt/errors).
* `Request`: the request ID, which identifies this stream. Give it to Rime support if you have a question about the stream.

Compare the final transcript with what you heard. Written mode, the default, changed the spoken "two four seven" to `247`. In this run, written mode also left out the caller's "uh" and "um".

> [!TIP]
>
> To compare latency, measure from the moment the script sends the last audio chunk to the final transcript. The total run time also includes starting the script, connecting to Rime, and reading and sending the file.

**6. Switch to word-for-word mode**

Word-for-word mode asks for the exact words of the speaker, including fillers such as "uh" and numbers as spoken words. Change the mode line in your script, and run the script on the same recording again:

**Python**

```python theme={null}
mode=TranscriptionMode.VERBATIM,
```

```bash theme={null}
uv run transcribe.py flight.pcm
```

**Node.js**

```javascript theme={null}
mode: "verbatim",
```

```bash theme={null}
node transcribe.mjs flight.pcm
```

**Go**

```go theme={null}
Mode:     rime.TranscriptionVerbatim,
```

```bash theme={null}
go run . flight.pcm
```

**Rust**

```rust theme={null}
let options = TranscriptionOptions::new("en").mode(TranscriptionMode::Verbatim);
```

```bash theme={null}
cargo run -- flight.pcm
```

These are the final lines from the two runs:

| Mode | Final transcript |
| - | - |
| Written (default) | `Hi! I'm calling about flight 247 to Denver. Can I, move it to Friday?` |
| Word-for-word | `Uh hi. I'm calling about flight two four seven to Denver, can I um move it to Friday?` |

Word-for-word mode kept the caller's "Uh" and "um", and it wrote the flight number as the caller said it: "two four seven". Written mode removed the fillers from this recording, but it does not promise to remove fillers. Use word-for-word mode when you need the exact wording as a record, for example in a quality or compliance review. [Recognition settings](/docs/stt/recognition#word-for-word-mode) explains how the two modes differ.

## Use your own recording

The stream call reads headerless PCM16 samples, not audio files. Convert your recording first.

1. Install [FFmpeg](https://ffmpeg.org/download.html).
2. Convert the recording to 16 kHz mono PCM16. This command reads `recording.mp3` and writes `recording.pcm`. A WAV file works the same way.

   ```bash theme={null}
   ffmpeg -i recording.mp3 -ac 1 -ar 16000 -c:a pcm_s16le -f s16le recording.pcm
   ```

   Renaming a file to `.pcm` does not convert it.
3. If the recording is in Spanish, set the language to `"es"` in the script. Use `language` in Python and Node.js, `Language` in Go, or `TranscriptionOptions::new("es")` in Rust.
4. Run the script with `recording.pcm` in place of `flight.pcm`.

Live phone audio, such as the 8 kHz mu-law of a Twilio media stream, needs one more step, because the SDK does not decode mu-law. [Where audio flows](/docs/stt/live-audio#where-audio-flows) shows how to decode it and declare its rate.

## Transcribe a whole file in one call

For a recording that you already have, such as a voicemail, the SDK can send the whole WAV file in one call. The call returns one final transcript and no partial transcripts. It takes the file with its WAV header, so it reads `flight.wav` as you downloaded it. One call takes a file of up to 16 MiB, with up to about 100 to 120 seconds of speech, as [Limits](/docs/stt/errors#limits) explains.

Save this script next to the first one:

**Python**

```python transcribe_file.py theme={null}
import asyncio
import sys
from pathlib import Path

from rimelabs_sdk import Rime, TranscriptionMode


async def main(path):
    audio = await asyncio.to_thread(Path(path).read_bytes)
    async with Rime() as client:
        result = await client.stt.transcribe(
            audio,
            language="en",
            mode=TranscriptionMode.WRITTEN,
        )
        print(f"final: {result.text}")
        print(f"Request: {result.request_id}")


asyncio.run(main(sys.argv[1]))
```

```bash theme={null}
uv run transcribe_file.py flight.wav
```

**Node.js**

```javascript transcribe-file.mjs theme={null}
import { readFile } from "node:fs/promises";
import { Rime } from "@rimelabs/sdk";

const client = new Rime();
try {
  const audio = await readFile(process.argv[2]);
  const result = await client.stt.transcribe(audio, {
    language: "en",
    mode: "written",
  });
  console.log(`final: ${result.text}`);
  console.log(`Request: ${result.requestId}`);
} finally {
  await client.close();
}
```

```bash theme={null}
node transcribe-file.mjs flight.wav
```

**Go**

Save the script in a `file` folder, because the module already has a `main` function in `main.go`:

```go file/main.go theme={null}
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    rime "github.com/rimelabs/rime-sdk/go"
)

func main() {
    if err := transcribeFile(os.Args[1]); err != nil {
        log.Fatal(err)
    }
}

func transcribeFile(path string) error {
    audio, err := os.ReadFile(path)
    if err != nil {
        return err
    }

    client, err := rime.NewClient(rime.Config{})
    if err != nil {
        return err
    }
    defer client.Close()

    result, err := client.STT.Transcribe(context.Background(), audio, rime.TranscribeOptions{
        Language: "en",
        Mode:     rime.TranscriptionWritten,
    })
    if err != nil {
        return err
    }
    fmt.Println("final:", result.Text)
    fmt.Println("Request:", result.RequestID)
    return nil
}
```

```bash theme={null}
go run ./file flight.wav
```

**Rust**

Save the script as a Cargo example, so that `cargo run` still runs `src/main.rs`:

```rust examples/transcribe_file.rs theme={null}
use rimelabs_sdk::{Client, TranscribeOptions, TranscriptionMode};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).ok_or("missing file name")?;
    let audio = tokio::fs::read(path).await?;

    let client = Client::builder().build()?;
    let options = TranscribeOptions::new("en").mode(TranscriptionMode::Written);
    let result = client.stt().transcribe(audio, options).await?;
    println!("final: {}", result.text);
    println!("Request: {}", result.request_id.unwrap_or_default());
    client.close().await;
    Ok(())
}
```

```bash theme={null}
cargo run --example transcribe_file -- flight.wav
```

The script prints one line with the final transcript, and then the request ID:

```text theme={null}
final: Hi! I'm calling about flight 247 to Denver. Can I, move it to Friday?
Request: ef41d3af-116e-4dba-8660-de8aaba376f4
```

To transcribe your own file, convert it to a 16 kHz mono PCM16 WAV file first. This format is the only one that Rime guarantees:

```bash theme={null}
ffmpeg -i recording.mp3 -ac 1 -ar 16000 -c:a pcm_s16le recording.wav
```

For a longer recording, stream it with append fragments over the API, as [Transcribe a long recording](/api-reference/stt/websockets#transcribe-a-long-recording) describes.

## Use the API without the SDK

> **Without the SDK**
>
> [Choose how to connect](/docs/speech-to-text#choose-how-to-connect) helps you choose WebSocket or gRPC. The WebSocket reference ends with a [complete Python client](/api-reference/stt/websockets#complete-python-client) that streams a WAV file. For a short WAV file in one request, use gRPC [`Transcribe`](/api-reference/stt/grpc#transcribe). For a longer recording, see [Transcribe a long recording](/api-reference/stt/websockets#transcribe-a-long-recording).

## Next

> **[Stream live audio](/docs/stt/live-audio)**
>
> Replace the file with your live audio source. The stream call does not change.

> **[Recognition settings](/docs/stt/recognition)**
>
> Choose word-for-word or written text. Add the names that your callers say.


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