Views
No views yet
CohereLabs/cohere-transcribe-03-2026 fine-tuned to also emit speaker labels and word-aligned timestamps in a single decoder pass, while preserving the base model's transcription quality. It's a drop-in replacement when you need to know who said what and when on short-form audio (≤ 30 s), and pairs with our diarize_long_vllm helper for arbitrary-length recordings.Recommended deployment: vLLM — see Serving with vLLM. We measured 44× real-time end-to-end on a 10-min clip with one RTX 3090 (decode 113× RTF, embed 16 seg/s), and 249× peak throughput under concurrent load. Transformers works too and is shown first for a minimal example, but the vLLM path is what we run in production.
| Name | cohere-transcribe-diarize |
|---|---|
| Base model | CohereLabs/cohere-transcribe-03-2026 (Apache 2.0, 2 B params) |
| Architecture | conformer-based encoder–decoder, full fine-tune (no LoRA) |
| Input | audio waveform (16 kHz mono, resampled automatically). Maximum supported clip length: 30 s — longer audio should be processed with sliding windows (see below) |
| Output | special-token stream interleaving speaker IDs, timestamps, and transcribed text, e.g. <|spltoken0|><|t:0.0|> Welcome back to the show.<|t:2.4|><|spltoken1|><|t:2.4|> Thanks for having me.<|t:3.8|> |
| Vocabulary extensions | 8 speaker tokens (<|spltoken0|>…<|spltoken7|>) + 300 timestamp tokens at 100 ms resolution (<|t:0.0|>…<|t:29.9|>) |
| Languages |
Primary: English (the diarization + timestamp fine-tune was done exclusively on English supervision). Likely usable (untested by us): the other 13 languages the Cohere Transcribe base supports — Arabic, German, Greek, Spanish, French, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Vietnamese, Chinese (Mandarin). The base model's multilingual transcription weights are preserved, and the diarization head conditions on language-agnostic speaker acoustics, so segmentation and speaker IDs should transfer; word-level timestamp accuracy will be best on English. Pass the matching language code in the prompt ( <|de|>, <|fr|>, …) to switch.
|
| License | Apache 2.0 (inherited from base) |
pip install transformers==4.57.6 torch huggingface_hub soundfile librosa sentencepiece protobuf1import re
2import torch
3from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq
4from transformers.audio_utils import load_audio
5
6MODEL_ID = "syvai/cohere-transcribe-diarize"
7
8processor = AutoProcessor.from_pretrained(MODEL_ID)
9model = AutoModelForSpeechSeq2Seq.from_pretrained(
10 MODEL_ID, dtype=torch.bfloat16
11).to("cuda").eval()
12
13# Prompt that activates diarization + timestamps. The base Cohere model
14# uses special control tokens to switch features on/off; we keep that contract.
15# `<|en|><|en|>` is the canonical Cohere prompt — the two slots are
16# audio-language + transcript-language; setting them to the same code means
17# "transcribe" (different codes would be "translate"). To run on another
18# Cohere language, swap BOTH tokens, e.g. `<|de|><|de|>`.
19# Each `<|...|>` is a single special token in the tokenizer vocab. Resolve
20# via convert_tokens_to_ids — running the prompt string through the tokenizer
21# re-tokenizes each marker into 6-12 subword pieces, which weakens the
22# control-token signal the model trained on.
23PROMPT_TOKENS = [
24 "<|startofcontext|>", "<|startoftranscript|>",
25 "<|emo:undefined|>", "<|en|>", "<|en|>",
26 "<|pnc|>", "<|noitn|>", "<|timestamp|>", "<|diarize|>",
27]
28prompt_ids = torch.tensor(
29 [[processor.tokenizer.convert_tokens_to_ids(t) for t in PROMPT_TOKENS]]
30).to(model.device)
31
32# Load any ≤ 30 s audio clip.
33audio = load_audio("clip.wav", sampling_rate=16000)
34inputs = processor(audio, sampling_rate=16000, return_tensors="pt")
35inputs = {k: v.to(model.device, dtype=model.dtype if v.is_floating_point() else None)
36 for k, v in inputs.items()}
37
38with torch.inference_mode():
39 out = model.generate(
40 input_features=inputs["input_features"],
41 attention_mask=torch.ones(inputs["input_features"].shape[:2], device=model.device),
42 decoder_input_ids=prompt_ids,
43 max_new_tokens=400,
44 do_sample=False,
45 repetition_penalty=1.2, # baked into generation_config but explicit here
46 )
47
48raw = processor.tokenizer.decode(out[0], skip_special_tokens=False)
49print(raw)
50# → <|spltoken0|><|t:0.0|> Welcome back. <|t:1.5|><|spltoken1|><|t:1.5|> Thanks. <|t:2.4|>...1SEG_RE = re.compile(r"<\|spltoken(\d+)\|><\|t:(\d+\.\d+)\|>(.*?)<\|t:(\d+\.\d+)\|>", re.DOTALL)
2
3# Drop the prompt prefix; the diarized text follows <|diarize|>
4text = raw.split("<|diarize|>", 1)[-1].replace("<|endoftext|>", "")
5
6segments = [
7 {
8 "speaker": int(m.group(1)),
9 "start": float(m.group(2)),
10 "end": float(m.group(4)),
11 "text": re.sub(r"<\|[^|]+\|>", "", m.group(3)).strip(),
12 }
13 for m in SEG_RE.finditer(text)
14]
15for s in segments:
16 print(f"[{s['start']:6.2f}–{s['end']:6.2f}] SPK{s['speaker']:02d} {s['text']}")1[ 0.00– 1.50] SPK00 Welcome back.
2[ 1.50– 2.40] SPK01 Thanks for having me.
3[ 2.40– 3.80] SPK00 Let's get into it.<|spltoken0|>…<|spltoken7|>). IDs are local to the clip — there is no global identity across separately decoded clips. For long-form audio that's split into windows, re-link windows with the helper below.diarize_long_vllm.py — recommended. Calls a local vLLM server concurrently (continuous batching) and reuses one GPU for both decode and embedding. ~44× RTF on a 10-min clip on a single 3090.diarize_long.py — transformers-only fallback, no server needed. Slower (~7× RTF on the same clip) but minimal deps.torch.hub)1# Assumes vLLM is already serving (see next section)
2python diarize_long_vllm.py podcast.wav \
3 --vllm http://127.0.0.1:8000 \
4 --model syvai/cohere-transcribe-diarize \
5 --language en \
6 --tau 0.45 \
7 --concurrency 32 \
8 --embed-batch 321from diarize_long import diarize_long_audio
2
3segments = diarize_long_audio(
4 audio="podcast.wav",
5 diar_model_id="syvai/cohere-transcribe-diarize",
6 language="en",
7 chunk_s=28.0,
8 overlap_s=2.0,
9 cluster_threshold=0.45,
10)numpy, scipy, soundfile, torchaudio (required by ReDimNet2's feature extractor), plus aiohttp if using diarize_long_vllm.py.cluster_threshold is the cosine-distance ceiling for AHC merges over ReDimNet2 embeddings. Around 0.45 is a good default for podcast / panel-style audio: a 2-min Bernie Sanders town-hall clip cleanly resolves Bernie as one consistent ID across all 5 sliding windows and the host as a second ID, while short audience interjections get their own IDs. Drop to 0.30–0.35 if the audio has many similar-sounding speakers; raise to 0.50–0.55 for noisier conditions where you'd rather collapse near-duplicate IDs.diarized_json response format, and ~25× higher peak throughput than calling model.generate() in a loop.1# Download the model locally first, then patch it
2hf download syvai/cohere-transcribe-diarize --local-dir cohere-transcribe-diarize
3
4# 1. Reshape the checkpoint files for vLLM compatibility
5python fix_for_vllm.py ./cohere-transcribe-diarizefix_for_vllm.py makes three edits to your local copy:tokenizer_config.json: drops the legacy extra_special_tokens list (transformers 4.57+ expects a dict; the actual tokens are still in tokenizer.json).config.json: sets head.num_classes and transf_decoder.config_dict.vocab_size to 16684 (the resized vocab).model.safetensors: strips the model. weight-name prefix and drops the BatchNorm num_batches_tracked tensors vLLM's CohereAsr model doesn't register.1# 2. Install vLLM 0.19.0 (NOT 0.19.1 — broken)
2uv pip install "vllm==0.19.0" --torch-backend=cu128
3uv pip install librosa
4
5# 3. Patch vLLM's speech_to_text endpoint to add diarized_json
6python vllm_diarized_patch.pyvllm_diarized_patch.py applies five edits inside the installed vLLM (also idempotent):protocol.py — add "diarized_json" to the AudioResponseFormat enumprotocol.py — force skip_special_tokens=False in to_sampling_params so <|spltoken*|> and <|t:*|> survive into the response textspeech_to_text.py — let the validator accept response_format="diarized_json"speech_to_text.py — parse the raw token stream with the segment regex and return OpenAI-compatible {task, language, duration, text, segments:[{speaker, start, end, text}], speakers, usage} JSONapi_router.py — pass JSONResponse returns through unchanged (otherwise the diarized branch's return value gets misinterpreted as a streaming generator and the response body comes out empty)1vllm serve ./cohere-transcribe-diarize \
2 --served-model-name syvai/cohere-transcribe-diarize \
3 --trust-remote-code \
4 --host 127.0.0.1 --port 8000 \
5 --gpu-memory-utilization 0.55 # leaves ~10 GB for ReDimNet2 batching--gpu-memory-utilization 0.55 is the sweet spot on a 24 GB card when you also run ReDimNet2 on the same GPU for long-form. If you only need short-form decode (≤ 30 s, no cross-chunk linking), bump it to 0.85 for better KV cache headroom.1curl -X POST http://127.0.0.1:8000/v1/audio/transcriptions \
2 -F "file=@clip.wav" \
3 -F "model=syvai/cohere-transcribe-diarize" \
4 -F "language=en" \
5 -F "response_format=diarized_json" \
6 --form-string "prompt=<|startofcontext|><|startoftranscript|><|emo:undefined|><|en|><|en|><|pnc|><|noitn|><|timestamp|><|diarize|>"gpt-4o-transcribe-diarize):1{
2 "task": "transcribe",
3 "language": "en",
4 "duration": 28.0,
5 "text": "UM I REJECT THE IDEA I REALLY DO ...",
6 "segments": [
7 {"speaker": "SPEAKER_00", "start": 2.5, "end": 3.8, "text": "I REALLY DO"},
8 {"speaker": "SPEAKER_01", "start": 3.6, "end": 15.0, "text": "IT'S ONE OF THINGS THAT BOTHERS ME ..."},
9 {"speaker": "SPEAKER_02", "start": 15.5, "end": 28.0, "text": "IS RAISING A STARVATION MINIMUM WAGE ..."}
10 ],
11 "speakers": ["SPEAKER_00", "SPEAKER_01", "SPEAKER_02"],
12 "usage": {"type": "duration", "seconds": 28}
13}prompt field must be passed explicitly — vLLM's default prompt builder emits <|nodiarize|> which suppresses the speaker tokens.| Concurrency | Throughput |
|---|---|
| 1 | 22× audio/wall |
| 8 | 117× |
| 32 | 171× |
| 128 | 249× (peak) |
CohereLabs/cohere-transcribe-03-2026 on English diarization data. The base vocabulary was extended with 8 speaker tokens and 300 100 ms timestamp tokens; the new rows of the embedding and LM-head matrices were initialised from the existing token embedding statistics.| Dataset | Rows | Description |
|---|---|---|
| AMI SDM (train split) | 19,928 | Single-distant-microphone meeting recordings, sliding 28 s windows with 14 s hop, up to 4 simultaneous speakers per window. Provides realistic multi-speaker conversation with overlap, hesitations, and turn-taking. |
| LibriSpeech synthetic mix | 11,813 | Synthetic K-speaker mixtures (K weighted 0.2 / 0.3 / 0.3 / 0.2 for K=1…4) constructed from LibriSpeech utterances, with realistic gap silences. Provides clean cross-talk-free speaker examples to anchor the diarization head. |
| Total | 31,741 | All segments are ≤ 30 s and capped at K ≤ 4 speakers. |
repetition_penalty=1.2 is baked into the generation config and is required at inference — without it, K=4 outputs occasionally loop on a single speaker token.diarize_long for longer audio. The Cohere feature extractor batches longer clips into multiple chunks, which the diarization decoder is not trained to consume.1@misc{cohere-transcribe-diarize-2026,
2 author = {{syv.ai}},
3 title = {Cohere Transcribe — Diarize + Timestamps (English)},
4 year = {2026},
5 publisher = {Hugging Face},
6 howpublished = {\url{https://huggingface.co/syvai/cohere-transcribe-diarize}},
7}