Chorus-v1 / README.md
RonanMcGovern's picture
Add link for Trelis Multi-speaker Transcription
6c048a4 verified
|
Raw
History Blame Contribute Delete
7.91 kB
---
license: apache-2.0
base_model: openai/whisper-large-v3-turbo
tags:
- automatic-speech-recognition
- multi-speaker
- whisper
- chorus
language:
- en
datasets:
- Trelis/ami-2speaker-test
library_name: transformers
pipeline_tag: automatic-speech-recognition
---
# Trelis Chorus v1
> Chorus v1 is a concept demo. For a production endpoint, consider [Trelis Multi-speaker Transcription](https://router.trelis.com/models).
**Need a voice model for your domain?** Trelis builds custom ASR, TTS, and voice agent pipelines for specialist verticals (legal, medical, finance, construction) and low-resource languages. [Enquire or book a consultation →](https://trelis.com/voice-ai-services/)
**Trelis Chorus v1** is a multi-speaker fine-tune of [`openai/whisper-large-v3-turbo`](https://huggingface.co/openai/whisper-large-v3-turbo). Given a mono audio clip of up to 30s containing two people speaking (possibly overlapping), it returns a separate transcript for each speaker — with timestamps — by conditioning the decoder on a `<|speaker1|>` or `<|speaker2|>` token.
## Typical use cases
- **Meeting transcription.** Two-person calls, interviews, or podcast segments where speakers overlap. Chorus returns a separate timestamped transcript per speaker without an upstream diarization step.
- **Clean single-speaker transcripts from imperfect isolation.** If you already have a close-mic recording of the speaker you care about but there's audible cross-talk from the other party, running Chorus with the `<|speaker1|>` token (first-to-speak in the clip) gives a transcript that **omits the other speaker's words** — cleaner than vanilla Whisper, which mixes both.
## Highlights
- Single forward pass per speaker (two passes for a 2-speaker transcript), speaker1 is the first to speak, speaker2 is the second to speak.
- Native timestamp emission — segments are emitted as `<|start|> text <|end|>` pairs
- Handles overlapping speech up to ~80% overlap in our AMI evaluation
- Keeps vanilla Whisper's output style (mixed-case English with punctuation, acronyms preserved)
- `speaker1` = first speaker to begin talking in the clip; `speaker2` = the other
Illustrative example of templating for training and inference:
- `<|speaker1|>First speaker utterances`
- `<|speaker2|>Second speaker utterances`
During training and inference, the model gets a separate forward pass for each speaker. The model learns to associate only the first speakers audio (including later turns) with the speaker1 token, and the second speaker's speech with the speaker2 token.
## Benchmark
Evaluated on [`Trelis/ami-2speaker-test`](https://huggingface.co/datasets/Trelis/ami-2speaker-test) — 50 real AMI meeting clips reconstructed as 2-speaker audio (average 20s, average overlap ~30%, up to 78%).
| Metric | Speaker 1 | Speaker 2 | Mean |
|---|---|---|---|
| CER | 8.58% | 10.12% | **9.35%** |
| CMER (bounded) | 8.32% | 9.69% | **9.00%** |
CER is standard character error rate via `whisper_normalizer` + `jiwer`. CMER = `(S+D+I) / (H+S+D+I)`, bounded to [0, 1]; more stable than CER for conversational speech with heavy deletions/insertions.
Per-row predictions browseable at [`Trelis/chorus-v1-ami-2speaker-test-preds`](https://huggingface.co/datasets/Trelis/chorus-v1-ami-2speaker-test-preds).
## Usage via Hosted API (Trelis Router)
Chorus is available as a hosted GPU endpoint on [Trelis Router](https://router.trelis.com) — no GPU setup, handles long audio end-to-end (VAD chunking + cross-chunk speaker clustering).
- Model id: `trelis/chorus-v1` · Base URL: `https://router.trelis.com/api/v1`
- Files up to 100 MB · Output: `json` / `srt` / `vtt` / `text`
```bash
curl -X POST https://router.trelis.com/api/v1/transcribe \
-H "Authorization: Bearer $TRELIS_ROUTER_API_KEY" \
-F model=trelis/chorus-v1 \
-F file=@meeting.wav
```
## Open Weights Usage
### Transformers
```python
import torch
import soundfile as sf
from transformers import WhisperForConditionalGeneration, WhisperProcessor
MODEL = "Trelis/Chorus-v1"
processor = WhisperProcessor.from_pretrained(MODEL)
model = (
WhisperForConditionalGeneration
.from_pretrained(MODEL, dtype=torch.float16)
.to("cuda")
.eval()
)
model.generation_config.predict_timestamps = True
model.generation_config.max_initial_timestamp_index = 1500 # allow up to 30s
tok = processor.tokenizer
ids = {n: tok.convert_tokens_to_ids(t) for n, t in [
("en", "<|en|>"), ("transcribe", "<|transcribe|>"),
("speaker1", "<|speaker1|>"), ("speaker2", "<|speaker2|>"),
]}
arr, sr = sf.read("your_clip.wav") # 16kHz mono, <= 30s
assert sr == 16_000
feats = processor.feature_extractor(
[arr], sampling_rate=16_000, return_tensors="pt"
).input_features.to("cuda").half()
for name in ["speaker1", "speaker2"]:
forced = [[1, ids["en"]], [2, ids["transcribe"]], [3, ids[name]]]
with torch.no_grad():
out = model.generate(
feats, forced_decoder_ids=forced,
return_timestamps=True, max_new_tokens=444,
)
print(f"{name}: {tok.decode(out[0], skip_special_tokens=True)}")
```
**Important**: `max_initial_timestamp_index=1500` is required — without it, HF's default caps the first emitted timestamp to 1.0s, which breaks Speaker 2 when they start talking later in the clip.
### vLLM
```python
import soundfile as sf
from vllm import LLM, SamplingParams
from vllm.inputs import TokensPrompt
MODEL = "Trelis/Chorus-v1"
llm = LLM(model=MODEL, dtype="float16", max_model_len=448, enforce_eager=True)
sampling = SamplingParams(temperature=0, max_tokens=200)
arr, sr = sf.read("your_clip.wav") # 16kHz mono, <= 30s
assert sr == 16_000
tok = llm.get_tokenizer()
for name in ["speaker1", "speaker2"]:
prefix = [
tok.convert_tokens_to_ids("<|startoftranscript|>"),
tok.convert_tokens_to_ids("<|en|>"),
tok.convert_tokens_to_ids("<|transcribe|>"),
tok.convert_tokens_to_ids(f"<|{name}|>"),
]
r = llm.generate(
prompts=[TokensPrompt(
prompt_token_ids=prefix,
multi_modal_data={"audio": (arr, 16_000)},
)],
sampling_params=sampling,
)
print(f"{name}: {r[0].outputs[0].text}")
```
vLLM output includes literal `<|N.NN|>` timestamp tokens in the returned text (parse with regex `<\|(\d+\.\d+)\|>`); transformers strips them via `skip_special_tokens=True`.
## Whisper.cpp
See [Trelis/Chorus-v1-GGML](https://huggingface.co/Trelis/Chorus-v1-GGML) for ggml quants and a modified whisper-cli.
## Limitations
- 2 speakers only. Not trained on 3+ speakers; behaviour is undefined there.
- English only. Not trained on other languages; Whisper's multilingual capability is retained in the encoder but decoder prompts use `<|en|>`.
- 30-second audio window (Whisper's fixed mel-spectrogram input). Chunk longer audio upstream.
- Conversational/meeting speech is the training target; very noisy recordings or heavy music background are out of distribution.
- `speaker1` is defined as *first-to-speak*; if both start simultaneously, the assignment is arbitrary.
## Training
Fine-tuned with LoRA (rank 32, α 16, rsLoRA) on ~17k rows: 10k speaker-leak-filtered synthetically-mixed VoxPopuli pairs + 7k real AMI IHM windows reconstructed as 2-speaker audio. Base checkpoint: `openai/whisper-large-v3-turbo`. One epoch, H100, ~90 min.
## License
Apache 2.0.
## Citation
If you use Chorus in research, please cite the AMI corpus (for the eval) and OpenAI Whisper (for the base model):
```
@inproceedings{carletta2006ami,
title={The AMI Meeting Corpus: A Pre-announcement},
author={Carletta, Jean and others},
booktitle={Machine Learning for Multimodal Interaction},
year={2006}
}
@article{radford2022whisper,
title={Robust Speech Recognition via Large-Scale Weak Supervision},
author={Radford, Alec and others},
journal={arXiv preprint arXiv:2212.04356},
year={2022}
}
```