ParaSpeechCLAP: A Dual-Encoder Speech-Text Model for Rich Stylistic Language-Audio Pretraining

April 6, 2026 · View on GitHub

[Paper] [HuggingFace Collection]

Official code and model release for the paper:

ParaSpeechCLAP: A Dual-Encoder Speech-Text Model for Rich Stylistic Language-Audio Pretraining
Anuj Diwan, Eunsol Choi, David Harwath
Under review

ParaSpeechCLAP is a CLAP-style dual-encoder model that maps speech and rich textual style descriptions into a common embedding space, supporting a wide range of intrinsic (speaker-level: pitch, texture, clarity, volume, rhythm) and situational (utterance-level: emotion, speaking style) descriptors.

Models

ModelDescriptionHuggingFace
ParaSpeechCLAP-IntrinsicSpecialized for intrinsic (speaker-level) style attributes. Trained with contrastive + classification loss and class-balanced sampling.ajd12342/paraspeechclap-intrinsic
ParaSpeechCLAP-SituationalSpecialized for situational (utterance-level) style attributes. Trained with contrastive loss.ajd12342/paraspeechclap-situational
ParaSpeechCLAP-CombinedUnified model trained on both intrinsic and situational data. Best for compositional style descriptions.ajd12342/paraspeechclap-combined

Architecture: WavLM-Large (317M) speech encoder + Granite Embedding (278M) text encoder, with projection heads mapping to a shared 768-dimensional embedding space.

Datasets

All models are trained on ParaSpeechCaps.

DatasetHuggingFace
Intrinsic Trainingajd12342/paraspeechcaps-intrinsic-train
Situational Trainingajd12342/paraspeechcaps-situational-train
Intrinsic Evalajd12342/paraspeechclap-eval-intrinsic
Situational Evalajd12342/paraspeechclap-eval-situational
Combined Evalajd12342/paraspeechclap-eval-combined

Installation

git clone https://github.com/ajd12342/paraspeechclap.git
cd paraspeechclap
pip install -r requirements.txt

Download Models

mkdir -p checkpoints
huggingface-cli download ajd12342/paraspeechclap-intrinsic paraspeechclap-intrinsic.pth.tar --local-dir checkpoints
huggingface-cli download ajd12342/paraspeechclap-situational paraspeechclap-situational.pth.tar --local-dir checkpoints
huggingface-cli download ajd12342/paraspeechclap-combined paraspeechclap-combined.pth.tar --local-dir checkpoints

All examples below assume checkpoints are stored in ./checkpoints/.

Quick Start: Inference

Command-line

# Intrinsic (speaker-level): similarity with a style description
python scripts/inference.py \
  --checkpoint_path ./checkpoints/paraspeechclap-intrinsic.pth.tar \
  --audio_path /path/to/audio.wav \
  --text "A person speaks in a deep, guttural tone."

# Intrinsic: zero-shot classification across candidate styles
python scripts/inference.py \
  --checkpoint_path ./checkpoints/paraspeechclap-intrinsic.pth.tar \
  --audio_path /path/to/audio.wav \
  --candidates deep shrill nasal husky raspy

# Situational (utterance-level): similarity with an emotion/speaking-style description
python scripts/inference.py \
  --checkpoint_path ./checkpoints/paraspeechclap-situational.pth.tar \
  --audio_path /path/to/audio.wav \
  --text "A person is speaking in a whispered style."

# Situational: zero-shot classification across emotion/speaking-style candidates
python scripts/inference.py \
  --checkpoint_path ./checkpoints/paraspeechclap-situational.pth.tar \
  --audio_path /path/to/audio.wav \
  --candidates angry happy calm whispered enthusiastic saddened anxious

# Combined (compositional): similarity with a description mixing both attribute types
python scripts/inference.py \
  --checkpoint_path ./checkpoints/paraspeechclap-combined.pth.tar \
  --audio_path /path/to/audio.wav \
  --text "A person with a deep, raspy voice is speaking in a whispered style."

# Combined: zero-shot classification (intrinsic or situational candidates)
python scripts/inference.py \
  --checkpoint_path ./checkpoints/paraspeechclap-combined.pth.tar \
  --audio_path /path/to/audio.wav \
  --candidates angry happy calm whispered enthusiastic saddened anxious

Python

The model loading and audio preprocessing is the same for all ParaSpeechCLAP models. The only difference is the checkpoint path and what you query with.

import torch
import torchaudio
import torchaudio.transforms as T
from paraspeechclap.model import CLAP
from transformers import AutoTokenizer, Wav2Vec2FeatureExtractor

SPEECH_MODEL = "microsoft/wavlm-large"
TEXT_MODEL = "ibm-granite/granite-embedding-278m-multilingual"
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Load a ParaSpeechCLAP model — swap the checkpoint path for intrinsic/situational/combined
model = CLAP(
    speech_name=SPEECH_MODEL,
    text_name=TEXT_MODEL,
    embedding_dim=768,
)
state_dict = torch.load("./checkpoints/paraspeechclap-intrinsic.pth.tar", map_location=DEVICE)
model.load_state_dict(state_dict, strict=False)
model.to(DEVICE).eval()

# Initialize preprocessors
feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(SPEECH_MODEL)
tokenizer = AutoTokenizer.from_pretrained(TEXT_MODEL)

# Load and preprocess audio (resample to 16 kHz mono)
waveform, sr = torchaudio.load("path/to/audio.wav")
if sr != 16000:
    waveform = T.Resample(sr, 16000)(waveform)
if waveform.shape[0] > 1:
    waveform = waveform.mean(dim=0, keepdim=True)
audio = feature_extractor(
    waveform.squeeze(0), sampling_rate=16000, return_tensors="pt"
).input_values.to(DEVICE)  # (1, num_samples)

with torch.no_grad():
    audio_emb = model.get_audio_embedding(audio, normalize=True)  # (1, 768)

Intrinsic (speaker-level) styles

# Similarity with a free-form intrinsic description
text_tokens = tokenizer(
    "A person speaks in a deep, guttural tone.",
    return_tensors="pt", padding=True, truncation=True, max_length=512
)
text_tokens = {k: v.to(DEVICE) for k, v in text_tokens.items()}

with torch.no_grad():
    text_emb = model.get_text_embedding(text_tokens, normalize=True)  # (1, 768)
    similarity = (audio_emb @ text_emb.T).item()
    print(f"Similarity: {similarity:.4f}")

# Zero-shot classification across intrinsic candidate styles
candidates = ["deep", "shrill", "nasal", "husky", "raspy"]
prompts = [f"A person is speaking in a {s} style." for s in candidates]
tokens = tokenizer(prompts, return_tensors="pt", padding=True, truncation=True, max_length=512)
tokens = {k: v.to(DEVICE) for k, v in tokens.items()}

with torch.no_grad():
    text_embs = model.get_text_embedding(tokens, normalize=True)  # (5, 768)
    scores = (audio_emb @ text_embs.T).squeeze(0)  # (5,)
    pred = candidates[scores.argmax().item()]
    print(f"Predicted style: {pred}")

Situational (utterance-level) styles

# Similarity with a free-form situational description
text_tokens = tokenizer(
    "A person is speaking in a whispered style.",
    return_tensors="pt", padding=True, truncation=True, max_length=512
)
text_tokens = {k: v.to(DEVICE) for k, v in text_tokens.items()}

with torch.no_grad():
    text_emb = model.get_text_embedding(text_tokens, normalize=True)  # (1, 768)
    similarity = (audio_emb @ text_emb.T).item()
    print(f"Similarity: {similarity:.4f}")

# Zero-shot classification across situational candidate styles
candidates = ["angry", "happy", "calm", "whispered", "enthusiastic", "saddened", "anxious"]
prompts = [f"A person is speaking in a {s} style." for s in candidates]
tokens = tokenizer(prompts, return_tensors="pt", padding=True, truncation=True, max_length=512)
tokens = {k: v.to(DEVICE) for k, v in tokens.items()}

with torch.no_grad():
    text_embs = model.get_text_embedding(tokens, normalize=True)  # (7, 768)
    scores = (audio_emb @ text_embs.T).squeeze(0)  # (7,)
    pred = candidates[scores.argmax().item()]
    print(f"Predicted style: {pred}")

Combined (compositional): intrinsic + situational

# Similarity with a compositional description (intrinsic + situational)
text_tokens = tokenizer(
    "A person with a deep, raspy voice is speaking in a whispered style.",
    return_tensors="pt", padding=True, truncation=True, max_length=512
)
text_tokens = {k: v.to(DEVICE) for k, v in text_tokens.items()}

with torch.no_grad():
    text_emb = model.get_text_embedding(text_tokens, normalize=True)  # (1, 768)
    similarity = (audio_emb @ text_emb.T).item()
    print(f"Similarity: {similarity:.4f}")

# Zero-shot classification — works for both intrinsic and situational candidates
candidates = ["angry", "happy", "calm", "whispered", "enthusiastic", "saddened", "anxious"]
prompts = [f"A person is speaking in a {s} style." for s in candidates]
tokens = tokenizer(prompts, return_tensors="pt", padding=True, truncation=True, max_length=512)
tokens = {k: v.to(DEVICE) for k, v in tokens.items()}

with torch.no_grad():
    text_embs = model.get_text_embedding(tokens, normalize=True)  # (7, 768)
    scores = (audio_emb @ text_embs.T).squeeze(0)  # (7,)
    pred = candidates[scores.argmax().item()]
    print(f"Predicted style: {pred}")

Evaluation

Evaluation scripts require audio files to be present locally. Pass data.audio_root=/path/to/audio_root to point to a directory organized as ${audio_root}/{source}/, where {source} matches the source column in each dataset (e.g., voxceleb, expresso, ears). See the ParaSpeechCaps audio setup instructions for how to download and organize each source.

Intrinsic

# Retrieval (R@1, R@10, Median Rank)
python scripts/evaluate_retrieval.py \
  --config-name eval/retrieval \
  checkpoint_path=./checkpoints/paraspeechclap-intrinsic.pth.tar \
  data.dataset_name=ajd12342/paraspeechclap-eval-intrinsic \
  data.audio_root=/path/to/audio_root \
  meta.results=./results/retrieval/paraspeechclap-eval-intrinsic/ajd12342-paraspeechclap-intrinsic

# Per-attribute classification (UAR, Macro F1)
for attr in clarity pitch rhythm texture volume; do
  python scripts/evaluate_classification.py \
    --config-name eval/classification/${attr} \
    checkpoint_path=./checkpoints/paraspeechclap-intrinsic.pth.tar \
    data.audio_root=/path/to/audio_root \
    meta.results=./results/classification/paraspeechclap-eval-intrinsic/ajd12342-paraspeechclap-intrinsic/${attr}
done

Available classification configs: eval/classification/pitch, eval/classification/texture, eval/classification/volume, eval/classification/clarity, eval/classification/rhythm. Each loads the corresponding split from ajd12342/paraspeechclap-eval-intrinsic (e.g., classification_pitch).

Situational

# Retrieval (R@1, R@10, Median Rank)
python scripts/evaluate_retrieval.py \
  --config-name eval/retrieval \
  checkpoint_path=./checkpoints/paraspeechclap-situational.pth.tar \
  data.dataset_name=ajd12342/paraspeechclap-eval-situational \
  data.audio_root=/path/to/audio_root \
  meta.results=./results/retrieval/paraspeechclap-eval-situational/ajd12342-paraspeechclap-situational

# Classification (UAR, Macro F1 — 21 situational classes)
python scripts/evaluate_classification.py \
  --config-name eval/classification/situational \
  checkpoint_path=./checkpoints/paraspeechclap-situational.pth.tar \
  data.audio_root=/path/to/audio_root \
  meta.results=./results/classification/paraspeechclap-eval-situational/ajd12342-paraspeechclap-situational/

Combined

# Retrieval (R@1, R@10, Median Rank) — compositional descriptions
python scripts/evaluate_retrieval.py \
  --config-name eval/retrieval \
  checkpoint_path=./checkpoints/paraspeechclap-combined.pth.tar \
  data.dataset_name=ajd12342/paraspeechclap-eval-combined \
  data.audio_root=/path/to/audio_root \
  meta.results=./results/retrieval/paraspeechclap-eval-combined/ajd12342-paraspeechclap-combined

Training

Train ParaSpeechCLAP models using distributed data parallel (DDP) on 4 GPUs:

# ParaSpeechCLAP-Intrinsic (contrastive + classification loss, class-balanced sampling)
torchrun --nproc_per_node=4 scripts/train.py \
  --config-name train/intrinsic \
  data.audio_root=/path/to/audio_root \
  meta.results=./experiments

# ParaSpeechCLAP-Situational (contrastive loss)
torchrun --nproc_per_node=4 scripts/train.py \
  --config-name train/situational \
  data.audio_root=/path/to/audio_root \
  meta.results=./experiments

# ParaSpeechCLAP-Combined (contrastive loss, interleaved intrinsic + situational data)
torchrun --nproc_per_node=4 scripts/train.py \
  --config-name train/combined \
  data.audio_root=/path/to/audio_root \
  meta.results=./experiments

Best-of-N Reranking

Use ParaSpeechCLAP as an inference-time reward model to select the best speech clip from N candidates, typically generated by a style-prompted TTS model.

# Expects pre-generated candidate speech clips organized as:
# /path/to/tts_outputs/
#   iter_1/audios/0.wav, 1.wav, ...
#   iter_2/audios/0.wav, 1.wav, ...
#   ...
#   iter_10/audios/0.wav, 1.wav, ...
#   iter_1/input_descriptions.txt  (one style prompt per line)

# Select best candidates using ParaSpeechCLAP-Intrinsic
python scripts/best_of_n.py \
  checkpoint_path=./checkpoints/paraspeechclap-intrinsic.pth.tar \
  input_base_dir=/path/to/tts_outputs \
  output_dir_name=best_of_N_paraspeechclap_intrinsic

# Select best candidates using ParaSpeechCLAP-Situational
python scripts/best_of_n.py \
  checkpoint_path=./checkpoints/paraspeechclap-situational.pth.tar \
  input_base_dir=/path/to/tts_outputs \
  output_dir_name=best_of_N_paraspeechclap_situational

# Select best candidates using ParaSpeechCLAP-Combined
python scripts/best_of_n.py \
  checkpoint_path=./checkpoints/paraspeechclap-combined.pth.tar \
  input_base_dir=/path/to/tts_outputs \
  output_dir_name=best_of_N_paraspeechclap_combined

Repository Structure

paraspeechclap/
├── paraspeechclap/
│   ├── model.py                   # CLAP dual-encoder architecture
│   ├── loss.py                    # ClipLoss and MultiTaskLoss
│   ├── dataset.py                 # ParaSpeechCaps dataset loader
│   ├── utils.py                   # Collate functions, utilities
│   ├── balanced_sampler.py        # Class-balanced sampling
│   ├── evaluation_utils.py        # Model loading, metric computation
│   └── debug_utils.py             # Logging utilities
├── scripts/
│   ├── inference.py               # Simple inference script
│   ├── train.py                   # DDP training script
│   ├── evaluate_classification.py  # Classification eval
│   ├── evaluate_retrieval.py      # Retrieval eval
│   └── best_of_n.py              # Best-of-N reranking
├── configs/
│   ├── train/
│   │   ├── base.yaml                # Shared training defaults
│   │   ├── intrinsic.yaml
│   │   ├── situational.yaml
│   │   └── combined.yaml
│   ├── eval/
│   │   ├── base.yaml                # Shared evaluation defaults
│   │   ├── retrieval.yaml
│   │   └── classification/
│   │       ├── base.yaml            # Shared classification defaults
│   │       ├── rhythm.yaml
│   │       ├── texture.yaml
│   │       ├── pitch.yaml
│   │       ├── clarity.yaml
│   │       ├── volume.yaml
│   │       └── situational.yaml     # paraspeechclap-eval-situational (21 classes)
│   └── best_of_n/
│       └── base.yaml                # Best-of-N defaults
├── requirements.txt
└── README.md

Citation

@misc{diwan2026paraspeechclapdualencoderspeechtextmodel,
      title={ParaSpeechCLAP: A Dual-Encoder Speech-Text Model for Rich Stylistic Language-Audio Pretraining},
      author={Anuj Diwan and Eunsol Choi and David Harwath},
      year={2026},
      eprint={2603.28737},
      archivePrefix={arXiv},
      primaryClass={eess.AS},
      url={https://arxiv.org/abs/2603.28737},
}

Acknowledgements

This codebase builds on the following projects:

License

This project is released under the MIT License. See LICENSE for details.