Voxtral Mini 3B

Speech turned into text. Send a recording and get its transcript, with the language detected or named.

Speechl40sMistral AIApache-2.0mistralai/Voxtral-Mini-3B-2507

What runs

This is the whole app. The entry names the model and what serves it, and the build turns that into these files: there is nothing else to write.

model.py
"""Voxtral Mini 3B, generated from a declarative template."""

from __future__ import annotations

import base64
import tempfile

from pydantic import BaseModel, ConfigDict, Field

from runware_serverless import serve

MODEL = "mistralai/Voxtral-Mini-3B-2507"
REVISION = "3060fe34b35ba5d44202ce9ff3c097642914f8f3"
CACHE = "/weights"
SAMPLE_RATE = 16000
DEFAULT_LANGUAGE = None


class Request(BaseModel):
    # An input this model does not read is a line the caller believed was doing
    # something, so it is refused rather than dropped.
    model_config = ConfigDict(extra="forbid")

    audio: str = Field(description="The speech to transcribe, as a base64 audio file. Any container soundfile reads, so WAV, FLAC and OGG all work.")
    language: str | None = Field(
        default=DEFAULT_LANGUAGE,
        description="The language to transcribe as, as a two-letter code. Left out, the model detects it.",
    )
    max_tokens: int = Field(default=512, ge=1, le=4096, description="Upper bound on the transcript's length in tokens.")


class Transcribed(BaseModel):
    text: str = Field(description="The transcript.")
    seconds: float = Field(description="How long the audio ran, after decoding.")
    language: str | None = Field(description="The language it was transcribed as, or null where the model chose.")


@serve
class Transcription:
    def load(self) -> None:
        if hasattr(self, "model"):
            return

        # Imported here rather than at the top of the file. The build imports
        # this file with the platform's own dependencies, so nothing above
        # exists until a worker runs.
        import torch
        from transformers import VoxtralForConditionalGeneration, VoxtralProcessor

        self.torch = torch
        self.processor = VoxtralProcessor.from_pretrained(
            MODEL, revision=REVISION, cache_dir=CACHE
        )
        # Straight onto the card as it loads. A worker's host RAM is half its
        # card's VRAM, so a model assembled on the host and moved has a ceiling
        # below the card's own.
        self.model = VoxtralForConditionalGeneration.from_pretrained(
            MODEL,
            revision=REVISION,
            cache_dir=CACHE,
            dtype=torch.bfloat16,
            device_map="cuda",
        )

    def predict(self, request: Request) -> Transcribed:
        import soundfile

        raw = base64.b64decode(request.audio)

        # Written to a file and passed as a path, not decoded into an array.
        # Given a path the processor calls load_audio_as itself, with
        # force_mono and the sampling rate it wants, so it does the conversion
        # the model needs. Given an array it requires a "format" argument and
        # refuses without one, and any resampling is then ours to get right.
        with tempfile.NamedTemporaryFile(suffix=".wav") as handle:
            handle.write(raw)
            handle.flush()

            info = soundfile.info(handle.name)

            inputs = self.processor.apply_transcription_request(
                audio=handle.name,
                model_id=MODEL,
                language=request.language,
                sampling_rate=SAMPLE_RATE,
            ).to("cuda", dtype=self.torch.bfloat16)

            generated = self.model.generate(**inputs, max_new_tokens=request.max_tokens)

            # Only the tokens the model added. The request template is long,
            # and decoding the whole sequence returns the prompt with the
            # transcript buried at the end of it.
            new = generated[:, inputs.input_ids.shape[1]:]
            text = self.processor.batch_decode(new, skip_special_tokens=True)[0]

        return Transcribed(
            text=text.strip(),
            seconds=info.frames / info.samplerate,
            language=request.language,
        )
runware.yaml
configVersion: 1
id: voxtral-mini-3b
name: Voxtral Mini 3B
source:
  type: code
  entry: model.py
compute:
  gpuType: l40s
build:
  requirements:
    - torch==2.14.1
    - transformers==5.21.0
    - accelerate==1.15.0
    - soundfile==0.14.0
    - librosa==0.11.0
    - mistral-common==1.13.1
volumes:
  - /weights

Deploying

Deploy it as it is, or take the files above and deploy your own copy. Nothing you change reaches back to this page.

voxtral-mini-3bl40ssource
From a URL
runware serverless deploy --from https://runware.ai/docs/serverless/examples/voxtral-mini-3b.zip
From a clone
runware serverless deploy model.py \
  --id voxtral-mini-3b \
  --gpu-type l40s \
  --requirement torch==2.14.1 \
  --requirement transformers==5.21.0 \
  --requirement accelerate==1.15.0 \
  --requirement soundfile==0.14.0 \
  --requirement librosa==0.11.0 \
  --requirement mistral-common==1.13.1 \
  --volume /weights

Calling it

One request that works, against the app you just deployed. The fields are the ones the entry declares above, and anything it does not declare is refused rather than ignored.

import { createClient } from '@runware/sdk'

const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })

const task = await client.invoke({
  appId: 'voxtral-mini-3b',
  endpointPath: 'predict',
  payload: {
    request: {
      audio: '<base64 audio file>'
    }
  }
})
import asyncio
import os

from runware import Runware


async def main():
    client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
    try:
        task = await client.invoke(
            "voxtral-mini-3b",
            "predict",
            {
                "request": {
                    "audio": "<base64 audio file>"
                }
            },
        )
    finally:
        await client.close()


asyncio.run(main())
curl https://api.serverless.runware.ai/v1/apps/voxtral-mini-3b/invoke-async/predict \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "payload": {
      "request": {
        "audio": "<base64 audio file>"
      }
    }
  }'
cat <<'EOF' | runware serverless apps invoke voxtral-mini-3b predict -f -
{
  "request": {
    "audio": "<base64 audio file>"
  }
}
EOF
{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "payload": {
    "request": {
      "audio": "<base64 audio file>"
    }
  }
}

What it costs

This entry is pinned to l40s, which is the card it was tested on rather than the only one it runs on: --gpu-typedeploys it somewhere else. You pay for that card by the second while a worker holds it, and for nothing else. Pricing covers what meters, and the rates are published per GPU-second.