# How to Transcribe Audio with whisper.cpp in a Cloud Sandbox

> Clone whisper.cpp, build it with cmake, fetch base.en or small.en with its download script, and run whisper-cli on 16 kHz WAV. On a 4-vCPU default sandbox, base.en ran 13x faster than realtime and small.en 5x. whisper-server behind a private sbx host URL accepts file POSTs.

Source: https://sandboxer.com/guides/transcribe-audio-with-whisper-cpp-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Clone whisper.cpp, build it with cmake, fetch base.en or small.en with its download script, and run whisper-cli on 16 kHz WAV. On a 4-vCPU default sandbox, base.en ran 13x faster than realtime and small.en 5x. whisper-server behind a private sbx host URL accepts file POSTs.

## At a glance

Everything below ran on prod on 2026-10-27, CPU only, with [whisper.cpp](https://github.com/ggml-org/whisper.cpp) v1.9.5 (commit `231638a`). The test clip is a 198.7-second US government speech from Wikimedia Commons, which is public domain.

| Step | Command | What we saw |
| - | - | - |
| Build | `cmake -B build && cmake --build build -j --config Release` | 1 min 38 s on `default`, no extra packages |
| Get a model | `sh ./models/download-ggml-model.sh small.en` | 488 MB in 6 s (`base.en`: 148 MB in 2 s) |
| Transcribe the 11 s JFK sample | `whisper-cli -f samples/jfk.wav` | 1.3 s total with `base.en` |
| Transcribe 198.7 s of speech | `whisper-cli -m … -t 4 -f gb1.wav` | 15.3 s (`base.en`), 40.6 s (`small.en`) |
| Serve it over HTTP | `whisper-server … --host 0.0.0.0 --convert` + `sbx host $id 8080` | `401` without the token, `200` with it |
| POST an MP3 from a laptop | `curl -F file=@jfk.mp3 "$URL/inference?_token=…"` | Transcript back in 3.8–4.0 s |

## 1. Create a sandbox and build whisper.cpp

Every sandbox already has `git`, `cmake`, `gcc`, `make`, `curl` and `ffmpeg`, so the build needs no `apt` step. A `default` sandbox (4 vCPU, 8 GB, $0.036/hour) is enough for `base.en` and `small.en`.

```bash
id=$(sbx new --type default --name aeo-whisper-cpp-1 --json | jq -r 'select(.event == "ready") | .id')

sbx exec $id --timeout 600 '
  git clone --depth 1 https://github.com/ggml-org/whisper.cpp.git
  cd whisper.cpp
  time (cmake -B build && cmake --build build -j --config Release)'
```

```text
-- Found OpenMP: TRUE (found version "4.5")
-- Adding CPU backend variant ggml-cpu: -march=native
…
[100%] Built target whisper-server

real    1m38.416s
```

These are the build steps from the project's [README quick start](https://github.com/ggml-org/whisper.cpp#quick-start). The build is longer than a minute, so pass `--timeout`. For builds that run longer than synchronous commands allow, see [how to run a long job](/guides/run-a-long-job-in-a-cloud-sandbox).

## 2. Download a model and transcribe the JFK sample

The repo ships `samples/jfk.wav`, the 11-second "ask not" line from Kennedy's 1961 inaugural address.

```bash
sbx exec $id --timeout 600 '
  cd whisper.cpp
  sh ./models/download-ggml-model.sh base.en
  sh ./models/download-ggml-model.sh small.en
  ./build/bin/whisper-cli -m models/ggml-base.en.bin -f samples/jfk.wav'
```

```text
system_info: n_threads = 4 / 4 | … CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | BMI2 = 1 | AVX512 = 1 | … OPENMP = 1 |
main: processing 'samples/jfk.wav' (176000 samples, 11.0 sec), 4 threads, 1 processors, 5 beams + best of 5, lang = en, task = transcribe, timestamps = 1 ...

[00:00:00.000 --> 00:00:11.000]   And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country.

whisper_print_timings:     load time =   108.92 ms
whisper_print_timings:   encode time =   702.79 ms /     1 runs (   702.79 ms per run)
whisper_print_timings:    total time =  1307.67 ms
```

`whisper-cli` reads 16-bit, 16 kHz mono WAV. Convert anything else with `ffmpeg` first, as the README shows.

## 3. Transcribe a longer clip

We used `gb1` from the repo's own `make samples` list: President George W. Bush's address on the loss of Space Shuttle Columbia, a public-domain recording on [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:George_W_Bush_Columbia_FINAL.ogg).

```bash
sbx exec $id --timeout 300 '
  cd whisper.cpp
  curl -sL -A "whisper-test/1.0" -o samples/gb1.ogg \
    https://upload.wikimedia.org/wikipedia/commons/1/1f/George_W_Bush_Columbia_FINAL.ogg
  ffmpeg -loglevel error -y -i samples/gb1.ogg -ar 16000 -ac 1 -c:a pcm_s16le samples/gb1.wav
  ffprobe -v error -show_entries format=duration -of csv=p=0 samples/gb1.wav'
```

```text
198.734375
```

Our first `wget` of the file exited with code 8 (a server error) and left an empty file. A retry with `curl` and a `User-Agent` header worked. Wikimedia asks scripts to send one; see its [User-Agent policy](https://meta.wikimedia.org/wiki/User-Agent_policy).

Here is the start of the real `small.en` transcript (`-otxt` writes it to a `.txt` file):

```text
My fellow Americans, this day has brought terrible news and great sadness to our country.
At 9 o'clock this morning, Mission Control in Houston lost contact with our space shuttle Columbia.
A short time later, debris was seen falling from the skies above Texas.
The Columbia's lost. There are no survivors.
On board was a crew of seven. Colonel Rick Husband, Lieutenant Colonel Michael Anderson, Commander Laurel Clark,
Captain David Brown, Commander William McCool, Dr. Kultana Shavla, and Ilan Ramon, a colonel in the Israeli Air Force.
…
May God bless the grieving families and may God continue to bless America.
```

The model choice shows in the text. `base.en` wrote "One board was a crew of seven", "Elon Ramon", "Our entire nation agrees with you" and "Those of their courage, endearing, and idealism". `small.en` got all four right. Both misheard Kalpana Chawla's name. whisper.cpp has a `--prompt` option for an initial prompt, which you could seed with names. We didn't test it here.

## 4. Measure the realtime factor

The realtime factor (RTF) is processing time divided by audio length. Below 1 is faster than realtime. This script times `whisper-cli` end to end, including model load, for each model and thread count:

```bash title="~/bench.sh"
cd ~/whisper.cpp
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 samples/gb1.wav)
for m in base.en small.en; do
  for t in $THREADS; do
    s=$(date +%s.%N)
    ./build/bin/whisper-cli -m models/ggml-$m.bin -t $t -f samples/gb1.wav -otxt -of /tmp/gb1-$m-t$t >/dev/null 2>&1
    e=$(date +%s.%N)
    awk -v m=$m -v t=$t -v s=$s -v e=$e -v d=$dur 'BEGIN{w=e-s; printf "%-9s threads=%d wall=%6.1fs  RTF=%.3f\n", m, t, w, w/d}'
  done
done
```

```bash
sbx exec $id --timeout 600 'THREADS="1 2 4" bash ~/bench.sh'
```

Then we stopped the sandbox, resumed it as `large` (8 vCPU, 16 GB, $0.072/hour) and ran `THREADS="4 8" bash ~/bench.sh`. All runs use the CLI default of 5 beams.

| Size | Model | Threads | Wall time for 198.7 s | RTF | Speed vs realtime |
| - | - | - | - | - | - |
| `default` | `base.en` | 1 | 42.5 s | 0.214 | 4.7x |
| `default` | `base.en` | 2 | 22.2 s | 0.112 | 9.0x |
| `default` | `base.en` | 4 | 15.3 s | 0.077 | 13.0x |
| `default` | `small.en` | 1 | 142.9 s | 0.719 | 1.4x |
| `default` | `small.en` | 2 | 64.7 s | 0.326 | 3.1x |
| `default` | `small.en` | 4 | 40.6 s | 0.204 | 4.9x |
| `large` | `base.en` | 4 | 13.4 s | 0.068 | 14.8x |
| `large` | `base.en` | 8 | 8.9 s | 0.045 | 22.3x |
| `large` | `small.en` | 4 | 38.3 s | 0.193 | 5.2x |
| `large` | `small.en` | 8 | 25.2 s | 0.127 | 7.9x |

What the numbers say:

- **Set `-t` to the vCPU count.** The default is 4 threads, so on `large` you must pass `-t 8` to use the extra cores.
- **`large` costs twice as much per hour but was not twice as fast.** `small.en` went from 40.6 s to 25.2 s (1.6x). Pick `large` when you need lower latency per file. Pick `default` for the lowest cost per audio hour.
- **Short clips have a floor.** Whisper encodes 30-second windows, so the 11-second JFK clip took as long to encode as 30 seconds would: 0.75 s (`base.en`) and 2.25 s (`small.en`) at 4 threads.

Estimated cost per hour of audio, from the RTFs above and [per-second billing](/docs/billing): `small.en` on `default` at 4 threads takes about 12 minutes, roughly $0.007. `base.en` takes about 4.6 minutes, roughly $0.003. These are extrapolations from one clip, not a bulk run.

## 5. Serve it over HTTP on a private URL

whisper.cpp includes [`whisper-server`](https://github.com/ggml-org/whisper.cpp/tree/master/examples/server), a small HTTP server with an `/inference` endpoint. Its README warns that it accepts file uploads and runs `ffmpeg` on them, and says to run it in a sandbox without admin rights. Here it runs as the unprivileged `user` account inside its own VM.

Run it as a systemd service so it comes back after a cold boot:

```bash
sbx exec $id '
sudo tee /etc/systemd/system/whisper.service >/dev/null <<"UNIT"
[Unit]
Description=whisper.cpp server
After=network-online.target

[Service]
User=user
WorkingDirectory=/home/user/whisper.cpp
ExecStart=/home/user/whisper.cpp/build/bin/whisper-server -m models/ggml-small.en.bin -t 4 --host 0.0.0.0 --port 8080 --convert
Restart=always

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload && sudo systemctl enable --now whisper'
```

`--host 0.0.0.0` lets the hosting proxy reach it. `--convert` makes the server run `ffmpeg` on uploads, so you can send MP3 or OGG, not only WAV. Now give port 8080 a URL:

```bash
sbx host $id 8080 --title whisper
```

```json
{"ok":true,"type":"port.hosted","sandboxId":"sb_3spo3t1kbc","port":8080,"url":"https://patient-fearless-poplar-8080.box.sandboxer.com?_token=<token>"}
```

The URL is private. From a laptop, without the token:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -F file=@jfk.mp3 https://patient-fearless-poplar-8080.box.sandboxer.com/inference
```

```text
401
```

With the token in the query string, the POST goes through:

```bash
curl -s -w "\n%{http_code} total=%{time_total}s\n" -F file=@jfk.mp3 -F response_format=text \
  "https://patient-fearless-poplar-8080.box.sandboxer.com/inference?_token=$TOKEN"
```

```text
 And so, my fellow Americans, ask not what your country can do for you.
 Ask what you can do for your country.

200 total=3.810330s
```

Three runs took 3.81, 4.01 and 3.99 seconds. The same request sent from inside the sandbox took about 3.3 seconds, so most of the time is the `small.en` encode, not the network. `response_format=verbose_json` returns segments with start and end times and per-word probabilities. A POST to the token URL got `200` directly. A GET gets a `302` that sets the `__sbx_auth` cookie, which also works for later requests. The [hosting guide](/guides/give-a-sandbox-port-an-https-url) covers tokens and cookies in full.

A longer file works too. Uploading the 1.6 MB `gb1.ogg` to the same URL on `large` returned the full transcript in 32.3 seconds. The server uses greedy decoding by default, not the CLI's 5 beams.

## What survives stop and resume

We ran `sbx stop $id --wait`, then `sbx resume $id --type large`. The resume took 3.4 seconds and reported `memoryRestored: false`, because a resize boots from disk. After the boot:

- The clone, the build and both models were still on disk.
- The `whisper` service was `active` again because it is an enabled systemd unit. A server started by hand would not have come back.
- The hosted URL and its token were unchanged. The first POST after the resume returned `200` in 3.3 seconds.

The cmake output above shows the CPU backend is built with `-march=native`. Our resize landed on the same CPU model, so the binary ran unchanged. We did not test a resume onto a different CPU type.

## FAQ

### Does whisper.cpp need a GPU?

No. Everything on this page ran on CPU, and Sandboxer [sizes](/docs/machines) have no GPU. The build detected AVX2 and AVX-512 and used OpenMP across the vCPUs.

### Which model should I use on a CPU sandbox?

Start with `small.en` for English. On `default` it ran at 4.9x realtime and got names and phrases right that `base.en` missed. Use `base.en` when speed matters more than accuracy. Drop the `.en` for other languages.

### Why does a 10-second clip take as long as a 30-second one?

Whisper's encoder always works on a 30-second window. Short clips pay that full encode cost, so their realtime factor looks worse than a long file's.

### How do I send audio from my laptop?

POST it to the hosted URL with `curl -F file=@audio.mp3`, as shown above, with `--convert` on the server. To copy files into the sandbox instead, see [moving files in and out of a sandbox](/guides/move-files-in-and-out-of-a-sandbox).

## Related

- [How do I give a port in a cloud sandbox an HTTPS URL?](/guides/give-a-sandbox-port-an-https-url)
- [How to run a long job in a cloud sandbox](/guides/run-a-long-job-in-a-cloud-sandbox)
- [Machines and sizes](/docs/machines)
- [Hosting](/docs/hosting)
