AcousticBench is an audio-perception benchmark for Large Audio-Language Models (LALMs). This repository contains the evaluation harness, Dockerized model servers, and reference results for reproducing every number in the paper.
| Sub-task | Questions | Format |
|---|---|---|
| RAD (Relative Acoustic Discrimination) | 1,200 | 2-clip A/B |
| RSQ (Relative Speech Quality) | 1,000 | 2-clip A/B |
| SER-Classify (Emotion Classification) | 600 | 1-clip A/B |
| SER-Intensity (Emotion Intensity) | 400 | 2-clip A/B |
| SER-Classify-Text (transcript diagnostic) | 600 | text-only A/B |
3,200 scored + 600 diagnostic = 3,800 questions total.
Two API keys are needed regardless of which models you evaluate:
| Variable | Why it's needed | How to get |
|---|---|---|
OPENAI_API_KEY | The LLM judge (gpt-4o-mini) extracts A/B labels from every model response. Called on every run.py invocation, even for open-weight models. | platform.openai.com/api-keys |
HF_TOKEN | Downloads open-weight model weights from HuggingFace on first Docker run. | huggingface.co/settings/tokens |
Save them:
# OpenAI key — put in .env (auto-loaded by run.py)
cp example.env .env
# Edit .env and set OPENAI_API_KEY=sk-...
# HuggingFace token — save where Docker containers can find it
mkdir -p ~/.cache/huggingface
echo "hf_YOUR_TOKEN" > ~/.cache/huggingface/token
Additional keys (
GEMINI_API_KEY) are only needed if you want to run Gemini models specifically.
| Requirement | Details |
|---|---|
| Python | 3.11+ |
| OS | Linux x86_64 (Ubuntu 22.04+) |
| GPU | NVIDIA A100-80GB (paper config). Smaller GPUs work for smaller models — see VRAM column below |
| Docker | 24.0+ with NVIDIA Container Toolkit |
| Disk | ~80 GB (5 GB archive + model weights cache) |
nvidia-smi
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
API models (Gemini, GPT-4o, Cascade) need no GPU — just API keys and internet.
git clone <repo-url> && cd acousticbench-public
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[all]"
# Set up API keys (see "Required for all steps below" above)
cp example.env .env
# Edit .env and set OPENAI_API_KEY=sk-...
# Extract benchmark data (use the actual filename you received)
python3 -c "import zipfile; zipfile.ZipFile('YOUR_ARCHIVE.zip').extractall('archive')"
# Verify extraction
python run.py --verify-checksums --task all
# Expected: "All 3800 questions passed checksum verification."
Important: Run
source .venv/bin/activatein every new terminal session before usingrun.pyor any scripts.
Run any model on any task and get accuracy numbers.
export GEMINI_API_KEY=your_key
python run.py --model Gemini-3.1-Pro --task rad --max-samples 3 --seed 42
# Start the model server (builds Docker image on first run, downloads weights from HuggingFace)
./scripts/docker_serve.sh voxtral_mini_3b
# Export the endpoint (printed by the script)
export ACOUSTICBENCH_VOXTRAL_MINI_3B_ENDPOINT=http://localhost:8009
# Run the evaluation (--max-samples 3 for a quick test; omit for the full task)
python run.py --model Voxtral-Mini-3B --task rad --max-samples 3 --seed 42
# Stop when done
./scripts/docker_serve.sh voxtral_mini_3b --stop
| Flag | What it does | Paper setting |
|---|---|---|
--model NAME | Which model to evaluate | See Models below |
--task TASK | Which task: rad, rsq, ser_classify, ser_classify_text, ser_intensity, or all | all |
--runs N | Number of independent runs | 3 |
--seed S | Starting seed. With --runs 3, produces seeds S, S+1, S+2 | 42 (seeds 42, 43, 44) |
--concurrency N | Parallel inference threads | 8 for API, 1 for Docker |
--split all|train|eval | Data split. all = full set (main leaderboard), eval = held-out (LoRA lift table) | all |
--max-samples N | Limit to N questions (for quick testing) | omit |
--compare-reference | After running, compare results against reference_trajectories/ and print match rate | - |
results/<Model>-<task>-<backend>-seed<N>-<timestamp>/
responses.jsonl # one JSON line per question
config.json # run configuration
metrics.json # aggregate accuracy
This verifies that your local GPU produces the same answers we reported. Add --compare-reference to any run.py command and it will automatically compare each run against the matching file in reference_trajectories/.
./scripts/docker_serve.sh voxtral_mini_3b
export ACOUSTICBENCH_VOXTRAL_MINI_3B_ENDPOINT=http://localhost:8009
python run.py --model Voxtral-Mini-3B --task rad --max-samples 10 --seed 42 --compare-reference
# Expected output: "Reference comparison: 10/10 (100.0%) match ..."
| Scenario | Expected |
|---|---|
| Same GPU type as ours (A100-80GB) | 100% |
| Different GPU (e.g. H100, A6000) | 90-100% (greedy decoding + flash-attention FP nondeterminism flips ~0-2% of near-50/50 items) |
| API models (Gemini, GPT-4o) | 95-100% (provider-side nondeterminism even at temp 0) |
reference_trajectories/
<Model>/
<task>/
run_1.jsonl # seed 42
run_2.jsonl # seed 43
run_3.jsonl # seed 44
summary.json # mean accuracy, std, per-run breakdown
| Model | --model flag | Required env var |
|---|---|---|
| Gemini-3.1-Pro | Gemini-3.1-Pro | GEMINI_API_KEY |
| Gemini-3-Flash | Gemini-3-Flash | GEMINI_API_KEY |
| Gemini-2.5-Pro | Gemini-2.5-Pro | GEMINI_API_KEY |
| Gemini-2.5-Flash-Audio | Gemini-2.5-Flash-Audio | GEMINI_API_KEY |
| GPT-4o | GPT-4o | OPENAI_API_KEY |
| Cascade (ASR baseline) | Cascade-GPT4oTranscribe-GPT5.4 | OPENAI_API_KEY |
| Model | --model flag | Docker name | VRAM | Port |
|---|---|---|---|---|
| Qwen3-Omni | Qwen3-Omni | qwen3_omni | ~70 GB | 8001 |
| Voxtral-Small-24B | Voxtral-Small-24B | voxtral_small_24b | ~50 GB | 8008 |
| Kimi-Audio | Kimi-Audio | kimi_audio | ~20 GB | 8002 |
| MiniCPM-o-4.5 | MiniCPM-o-4.5 | minicpm_o | ~20 GB | 8005 |
| Step-Audio-2-mini | Step-Audio-2-mini | step_audio2 | ~20 GB | 8020 |
| MiMo-Audio-7B | MiMo-Audio-7B | mimo_audio | ~20 GB | 8019 |
| Audio-Flamingo-3 | Audio-Flamingo-3 | audio_flamingo3 | ~20 GB | 8010 |
| Qwen2-Audio-7B | Qwen2-Audio-7B-Instruct | qwen2_audio | ~16 GB | 8004 |
| Phi-4-Multimodal | Phi-4-Multimodal | phi4mm | ~12 GB | 8006 |
| Voxtral-Mini-3B | Voxtral-Mini-3B | voxtral_mini_3b | ~8 GB | 8009 |
Each model's endpoint env var is printed by docker_serve.sh when starting. See ./scripts/docker_serve.sh --list for the full list.
| Model | --model flag | Docker name |
|---|---|---|
| Kimi-Audio-LoRA-RAD | Kimi-Audio-LoRA-RAD | kimi_lora_rad |
| Kimi-Audio-LoRA-RSQ | Kimi-Audio-LoRA-RSQ | kimi_lora_rsq |
| Kimi-Audio-LoRA-SER-Classify | Kimi-Audio-LoRA-SER-Classify | kimi_lora_ser_classify |
| Kimi-Audio-LoRA-SER-Intensity | Kimi-Audio-LoRA-SER-Intensity | kimi_lora_ser_intensity |
| Kimi-Audio-LoRA-Simple-AudioQA | Kimi-Audio-LoRA-Simple-AudioQA | kimi_lora_simple_audioqa |
Requires Kimi LoRA adapter weights extracted into ./archive/. Download from Kaggle:
pip install kagglehub
python3 -c "
import kagglehub, shutil
path = kagglehub.dataset_download('blueberrystonekitten/acousticbench-kimi-rap-finetunes')
shutil.copytree(path, 'archive', dirs_exist_ok=True)
print('Adapter weights extracted to ./archive/')
"
Important: LoRA reference trajectories were run with
--split eval. Use--split evalwhen comparing against them. The LoRA Docker container needs ~60 GB of free disk on first boot to export the merged model (cached for subsequent runs).
Each run_*.jsonl (and each responses.jsonl from your own runs) has one JSON line per question:
| Field | Type | Description |
|---|---|---|
example_id | string | Stable ID matching archive/questions/acousticbench_<task>.jsonl |
response_text | string | Raw model output (empty if error) |
response_status | "ok" / "error" | Whether the model responded |
chosen | "A" / "B" / null | Answer extracted by the judge. null = unparseable or refusal |
correct | "A" / "B" | Gold answer |
is_correct | bool | chosen == correct (false when chosen is null) |
extraction_source | string | "deterministic" (regex), "rerun-attempt-N" (refusal retry), or "llm" (LLM re-judge) |
wall_time_s | float | Inference latency in seconds |
meta | object | Question metadata: dimension, category, task, etc. |
A small fraction of responses are abstentions or verbose text the judge can't parse. If you see null values in chosen:
# Retry refusals up to 3 times
python scripts/rerun_nones.py --model MODEL_NAME --concurrency 8 --max-retries 3 \
results/MODEL_NAME-*-seed42-*/responses.jsonl
# Re-judge remaining unparseable responses via LLM
python scripts/rejudge.py results/MODEL_NAME-*/responses.jsonl
Greedy decoding. All open-weight models use temperature=0, do_sample=False. Run-to-run variance is zero or near-zero. Small differences across GPU types come from flash-attention floating-point nondeterminism, not sampling.
Paper configuration. All reference trajectories were produced on A100-80GB GPUs, with 3 runs (seeds 42, 43, 44) per (model, task).
Train/eval splits. Each task has train and eval halves. Main leaderboard: --split all. LoRA lift table (paper Table 2): --split eval.
MiniCPM-o-4.5 merges paired audio clips with a 1-second silence gap for 2-clip tasks because its chat interface accepts only single audio input.
LLM judge. The harness uses gpt-4o-mini to extract A/B labels from free-text responses. Requires OPENAI_API_KEY.
Zip format. The benchmark archive uses Zip64. System unzip on many Linux distributions can't handle this — use the Python zipfile command shown above.
First Docker run. Downloads model weights from HuggingFace (8-70 GB depending on model). Weights cache at ~/.cache/huggingface and reuse on subsequent runs.
Python
94.0%
Dockerfile
4.3%
Shell
1.7%
AcousticBench is an audio-perception benchmark for Large Audio-Language Models (LALMs). This repository contains the evaluation harness, Dockerized model servers, and reference results for reproducing every number in the paper.
| Sub-task | Questions | Format |
|---|---|---|
| RAD (Relative Acoustic Discrimination) | 1,200 | 2-clip A/B |
| RSQ (Relative Speech Quality) | 1,000 | 2-clip A/B |
| SER-Classify (Emotion Classification) | 600 | 1-clip A/B |
| SER-Intensity (Emotion Intensity) | 400 | 2-clip A/B |
| SER-Classify-Text (transcript diagnostic) | 600 | text-only A/B |
3,200 scored + 600 diagnostic = 3,800 questions total.
Two API keys are needed regardless of which models you evaluate:
| Variable | Why it's needed | How to get |
|---|---|---|
OPENAI_API_KEY | The LLM judge (gpt-4o-mini) extracts A/B labels from every model response. Called on every run.py invocation, even for open-weight models. | platform.openai.com/api-keys |
HF_TOKEN | Downloads open-weight model weights from HuggingFace on first Docker run. | huggingface.co/settings/tokens |
Save them:
# OpenAI key — put in .env (auto-loaded by run.py)
cp example.env .env
# Edit .env and set OPENAI_API_KEY=sk-...
# HuggingFace token — save where Docker containers can find it
mkdir -p ~/.cache/huggingface
echo "hf_YOUR_TOKEN" > ~/.cache/huggingface/token
Additional keys (
GEMINI_API_KEY) are only needed if you want to run Gemini models specifically.
| Requirement | Details |
|---|---|
| Python | 3.11+ |
| OS | Linux x86_64 (Ubuntu 22.04+) |
| GPU | NVIDIA A100-80GB (paper config). Smaller GPUs work for smaller models — see VRAM column below |
| Docker | 24.0+ with NVIDIA Container Toolkit |
| Disk | ~80 GB (5 GB archive + model weights cache) |
nvidia-smi
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
API models (Gemini, GPT-4o, Cascade) need no GPU — just API keys and internet.
git clone <repo-url> && cd acousticbench-public
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[all]"
# Set up API keys (see "Required for all steps below" above)
cp example.env .env
# Edit .env and set OPENAI_API_KEY=sk-...
# Extract benchmark data (use the actual filename you received)
python3 -c "import zipfile; zipfile.ZipFile('YOUR_ARCHIVE.zip').extractall('archive')"
# Verify extraction
python run.py --verify-checksums --task all
# Expected: "All 3800 questions passed checksum verification."
Important: Run
source .venv/bin/activatein every new terminal session before usingrun.pyor any scripts.
Run any model on any task and get accuracy numbers.
export GEMINI_API_KEY=your_key
python run.py --model Gemini-3.1-Pro --task rad --max-samples 3 --seed 42
# Start the model server (builds Docker image on first run, downloads weights from HuggingFace)
./scripts/docker_serve.sh voxtral_mini_3b
# Export the endpoint (printed by the script)
export ACOUSTICBENCH_VOXTRAL_MINI_3B_ENDPOINT=http://localhost:8009
# Run the evaluation (--max-samples 3 for a quick test; omit for the full task)
python run.py --model Voxtral-Mini-3B --task rad --max-samples 3 --seed 42
# Stop when done
./scripts/docker_serve.sh voxtral_mini_3b --stop
| Flag | What it does | Paper setting |
|---|---|---|
--model NAME | Which model to evaluate | See Models below |
--task TASK | Which task: rad, rsq, ser_classify, ser_classify_text, ser_intensity, or all | all |
--runs N | Number of independent runs | 3 |
--seed S | Starting seed. With --runs 3, produces seeds S, S+1, S+2 | 42 (seeds 42, 43, 44) |
--concurrency N | Parallel inference threads | 8 for API, 1 for Docker |
--split all|train|eval | Data split. all = full set (main leaderboard), eval = held-out (LoRA lift table) | all |
--max-samples N | Limit to N questions (for quick testing) | omit |
--compare-reference | After running, compare results against reference_trajectories/ and print match rate | - |
results/<Model>-<task>-<backend>-seed<N>-<timestamp>/
responses.jsonl # one JSON line per question
config.json # run configuration
metrics.json # aggregate accuracy
This verifies that your local GPU produces the same answers we reported. Add --compare-reference to any run.py command and it will automatically compare each run against the matching file in reference_trajectories/.
./scripts/docker_serve.sh voxtral_mini_3b
export ACOUSTICBENCH_VOXTRAL_MINI_3B_ENDPOINT=http://localhost:8009
python run.py --model Voxtral-Mini-3B --task rad --max-samples 10 --seed 42 --compare-reference
# Expected output: "Reference comparison: 10/10 (100.0%) match ..."
| Scenario | Expected |
|---|---|
| Same GPU type as ours (A100-80GB) | 100% |
| Different GPU (e.g. H100, A6000) | 90-100% (greedy decoding + flash-attention FP nondeterminism flips ~0-2% of near-50/50 items) |
| API models (Gemini, GPT-4o) | 95-100% (provider-side nondeterminism even at temp 0) |
reference_trajectories/
<Model>/
<task>/
run_1.jsonl # seed 42
run_2.jsonl # seed 43
run_3.jsonl # seed 44
summary.json # mean accuracy, std, per-run breakdown
| Model | --model flag | Required env var |
|---|---|---|
| Gemini-3.1-Pro | Gemini-3.1-Pro | GEMINI_API_KEY |
| Gemini-3-Flash | Gemini-3-Flash | GEMINI_API_KEY |
| Gemini-2.5-Pro | Gemini-2.5-Pro | GEMINI_API_KEY |
| Gemini-2.5-Flash-Audio | Gemini-2.5-Flash-Audio | GEMINI_API_KEY |
| GPT-4o | GPT-4o | OPENAI_API_KEY |
| Cascade (ASR baseline) | Cascade-GPT4oTranscribe-GPT5.4 | OPENAI_API_KEY |
| Model | --model flag | Docker name | VRAM | Port |
|---|---|---|---|---|
| Qwen3-Omni | Qwen3-Omni | qwen3_omni | ~70 GB | 8001 |
| Voxtral-Small-24B | Voxtral-Small-24B | voxtral_small_24b | ~50 GB | 8008 |
| Kimi-Audio | Kimi-Audio | kimi_audio | ~20 GB | 8002 |
| MiniCPM-o-4.5 | MiniCPM-o-4.5 | minicpm_o | ~20 GB | 8005 |
| Step-Audio-2-mini | Step-Audio-2-mini | step_audio2 | ~20 GB | 8020 |
| MiMo-Audio-7B | MiMo-Audio-7B | mimo_audio | ~20 GB | 8019 |
| Audio-Flamingo-3 | Audio-Flamingo-3 | audio_flamingo3 | ~20 GB | 8010 |
| Qwen2-Audio-7B | Qwen2-Audio-7B-Instruct | qwen2_audio | ~16 GB | 8004 |
| Phi-4-Multimodal | Phi-4-Multimodal | phi4mm | ~12 GB | 8006 |
| Voxtral-Mini-3B | Voxtral-Mini-3B | voxtral_mini_3b | ~8 GB | 8009 |
Each model's endpoint env var is printed by docker_serve.sh when starting. See ./scripts/docker_serve.sh --list for the full list.
| Model | --model flag | Docker name |
|---|---|---|
| Kimi-Audio-LoRA-RAD | Kimi-Audio-LoRA-RAD | kimi_lora_rad |
| Kimi-Audio-LoRA-RSQ | Kimi-Audio-LoRA-RSQ | kimi_lora_rsq |
| Kimi-Audio-LoRA-SER-Classify | Kimi-Audio-LoRA-SER-Classify | kimi_lora_ser_classify |
| Kimi-Audio-LoRA-SER-Intensity | Kimi-Audio-LoRA-SER-Intensity | kimi_lora_ser_intensity |
| Kimi-Audio-LoRA-Simple-AudioQA | Kimi-Audio-LoRA-Simple-AudioQA | kimi_lora_simple_audioqa |
Requires Kimi LoRA adapter weights extracted into ./archive/. Download from Kaggle:
pip install kagglehub
python3 -c "
import kagglehub, shutil
path = kagglehub.dataset_download('blueberrystonekitten/acousticbench-kimi-rap-finetunes')
shutil.copytree(path, 'archive', dirs_exist_ok=True)
print('Adapter weights extracted to ./archive/')
"
Important: LoRA reference trajectories were run with
--split eval. Use--split evalwhen comparing against them. The LoRA Docker container needs ~60 GB of free disk on first boot to export the merged model (cached for subsequent runs).
Each run_*.jsonl (and each responses.jsonl from your own runs) has one JSON line per question:
| Field | Type | Description |
|---|---|---|
example_id | string | Stable ID matching archive/questions/acousticbench_<task>.jsonl |
response_text | string | Raw model output (empty if error) |
response_status | "ok" / "error" | Whether the model responded |
chosen | "A" / "B" / null | Answer extracted by the judge. null = unparseable or refusal |
correct | "A" / "B" | Gold answer |
is_correct | bool | chosen == correct (false when chosen is null) |
extraction_source | string | "deterministic" (regex), "rerun-attempt-N" (refusal retry), or "llm" (LLM re-judge) |
wall_time_s | float | Inference latency in seconds |
meta | object | Question metadata: dimension, category, task, etc. |
A small fraction of responses are abstentions or verbose text the judge can't parse. If you see null values in chosen:
# Retry refusals up to 3 times
python scripts/rerun_nones.py --model MODEL_NAME --concurrency 8 --max-retries 3 \
results/MODEL_NAME-*-seed42-*/responses.jsonl
# Re-judge remaining unparseable responses via LLM
python scripts/rejudge.py results/MODEL_NAME-*/responses.jsonl
Greedy decoding. All open-weight models use temperature=0, do_sample=False. Run-to-run variance is zero or near-zero. Small differences across GPU types come from flash-attention floating-point nondeterminism, not sampling.
Paper configuration. All reference trajectories were produced on A100-80GB GPUs, with 3 runs (seeds 42, 43, 44) per (model, task).
Train/eval splits. Each task has train and eval halves. Main leaderboard: --split all. LoRA lift table (paper Table 2): --split eval.
MiniCPM-o-4.5 merges paired audio clips with a 1-second silence gap for 2-clip tasks because its chat interface accepts only single audio input.
LLM judge. The harness uses gpt-4o-mini to extract A/B labels from free-text responses. Requires OPENAI_API_KEY.
Zip format. The benchmark archive uses Zip64. System unzip on many Linux distributions can't handle this — use the Python zipfile command shown above.
First Docker run. Downloads model weights from HuggingFace (8-70 GB depending on model). Weights cache at ~/.cache/huggingface and reuse on subsequent runs.
Python
94.0%
Dockerfile
4.3%
Shell
1.7%