Automatic Speech Recognition
Transformers
asr
speaker-diarization
timestamps
quantization
low-bit
arm
on-device
Instructions to use yongyizang/TinyMOSS-Diarize with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use yongyizang/TinyMOSS-Diarize with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("automatic-speech-recognition", model="yongyizang/TinyMOSS-Diarize")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("yongyizang/TinyMOSS-Diarize", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| """Speaker-neutral grammar constraints for MOSS transcript generation. | |
| The constraint enforces only the wire syntax. It never chooses a speaker, | |
| timestamp, or text token on the model's behalf. Token transitions operate on | |
| decoded token pieces so merged BPE tokens such as ``][`` and ``[S`` remain | |
| valid when they cross grammar boundaries. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import hashlib | |
| import time | |
| from dataclasses import dataclass | |
| from typing import Iterable | |
| import torch | |
| from transformers import LogitsProcessor | |
| SCHEMA = "moss-transcript-grammar-v1" | |
| class GrammarState: | |
| phase: str = "start" | |
| def _digit(value: str) -> bool: | |
| return "0" <= value <= "9" | |
| def advance_character(state: GrammarState, char: str) -> GrammarState | None: | |
| """Advance one Unicode character, or return ``None`` for invalid syntax.""" | |
| phase = state.phase | |
| if phase == "start": | |
| return GrammarState("start_time_first") if char == "[" else None | |
| if phase == "start_time_first": | |
| return GrammarState("start_time_int") if _digit(char) else None | |
| if phase == "start_time_int": | |
| if _digit(char): | |
| return state | |
| if char == ".": | |
| return GrammarState("start_time_frac_first") | |
| if char == "]": | |
| return GrammarState("after_start_time") | |
| return None | |
| if phase == "start_time_frac_first": | |
| return GrammarState("start_time_frac") if _digit(char) else None | |
| if phase == "start_time_frac": | |
| if _digit(char): | |
| return state | |
| return GrammarState("after_start_time") if char == "]" else None | |
| if phase == "after_start_time": | |
| if char.isspace(): | |
| return state | |
| return GrammarState("speaker_s") if char == "[" else None | |
| if phase == "speaker_s": | |
| return GrammarState("speaker_digit_1") if char == "S" else None | |
| if phase == "speaker_digit_1": | |
| return GrammarState("speaker_digit_2") if _digit(char) else None | |
| if phase == "speaker_digit_2": | |
| return GrammarState("speaker_close") if _digit(char) else None | |
| if phase == "speaker_close": | |
| return GrammarState("text_empty") if char == "]" else None | |
| if phase == "text_empty": | |
| if char in "[]": | |
| return None | |
| return state if char.isspace() else GrammarState("text") | |
| if phase == "text": | |
| if char == "[": | |
| return GrammarState("end_time_first") | |
| if char == "]": | |
| return None | |
| return state | |
| if phase == "end_time_first": | |
| return GrammarState("end_time_int") if _digit(char) else None | |
| if phase == "end_time_int": | |
| if _digit(char): | |
| return state | |
| if char == ".": | |
| return GrammarState("end_time_frac_first") | |
| if char == "]": | |
| return GrammarState("after_end_time") | |
| return None | |
| if phase == "end_time_frac_first": | |
| return GrammarState("end_time_frac") if _digit(char) else None | |
| if phase == "end_time_frac": | |
| if _digit(char): | |
| return state | |
| return GrammarState("after_end_time") if char == "]" else None | |
| if phase == "after_end_time": | |
| if char.isspace(): | |
| return state | |
| return GrammarState("start_time_first") if char == "[" else None | |
| raise ValueError(f"unknown transcript grammar phase: {phase}") | |
| def advance_piece(state: GrammarState, piece: str) -> GrammarState | None: | |
| if not piece: | |
| return None | |
| for char in piece: | |
| state = advance_character(state, char) | |
| if state is None: | |
| return None | |
| return state | |
| def accepting(state: GrammarState) -> bool: | |
| return state.phase == "after_end_time" | |
| class TranscriptGrammarVocabulary: | |
| """Tokenizer-specific transition cache shared across utterances.""" | |
| def __init__(self, tokenizer): | |
| self.tokenizer = tokenizer | |
| self.vocab_size = len(tokenizer) | |
| eos = tokenizer.eos_token_id | |
| self.eos_token_ids = {int(value) for value in (eos if isinstance(eos, list) else [eos])} | |
| self.special_ids = {int(value) for value in getattr(tokenizer, "all_special_ids", [])} | |
| self.pieces = tuple( | |
| tokenizer.decode( | |
| [token_id], skip_special_tokens=False, clean_up_tokenization_spaces=False | |
| ) | |
| for token_id in range(self.vocab_size) | |
| ) | |
| digest = hashlib.sha256() | |
| for token_id, piece in enumerate(self.pieces): | |
| digest.update(str(token_id).encode("ascii")) | |
| digest.update(b"\0") | |
| digest.update(piece.encode("utf-8", errors="surrogatepass")) | |
| digest.update(b"\n") | |
| self.token_surface_sha256 = digest.hexdigest() | |
| self._allowed_cpu: dict[GrammarState, tuple[int, ...]] = {} | |
| self._allowed_device: dict[tuple[GrammarState, str], torch.Tensor] = {} | |
| def consume(self, token_ids: Iterable[int]) -> GrammarState: | |
| state = GrammarState() | |
| for token_id in token_ids: | |
| token_id = int(token_id) | |
| if token_id in self.eos_token_ids: | |
| if not accepting(state): | |
| raise ValueError("EOS before a complete end timestamp") | |
| continue | |
| if token_id in self.special_ids or not 0 <= token_id < self.vocab_size: | |
| raise ValueError(f"invalid special/out-of-vocabulary token in transcript: {token_id}") | |
| next_state = advance_piece(state, self.pieces[token_id]) | |
| if next_state is None: | |
| raise ValueError( | |
| f"token {token_id} piece={self.pieces[token_id]!r} violates phase={state.phase}" | |
| ) | |
| state = next_state | |
| return state | |
| def allowed_token_ids(self, state: GrammarState, device: torch.device) -> torch.Tensor: | |
| if state not in self._allowed_cpu: | |
| allowed = [] | |
| for token_id, piece in enumerate(self.pieces): | |
| if token_id in self.special_ids: | |
| continue | |
| if advance_piece(state, piece) is not None: | |
| allowed.append(token_id) | |
| # An empty response is not a valid transcript for deployment and | |
| # the parser cannot score it. EOS therefore becomes available | |
| # only after the model has completed a full segment. | |
| if accepting(state): | |
| allowed.extend(self.eos_token_ids) | |
| if not allowed: | |
| raise RuntimeError(f"transcript grammar has no continuation from {state.phase}") | |
| self._allowed_cpu[state] = tuple(sorted(set(allowed))) | |
| key = (state, str(device)) | |
| if key not in self._allowed_device: | |
| self._allowed_device[key] = torch.tensor( | |
| self._allowed_cpu[state], dtype=torch.long, device=device | |
| ) | |
| return self._allowed_device[key] | |
| def audit(self) -> dict: | |
| return { | |
| "schema": SCHEMA, | |
| "speaker_policy": "model_selected_exact_two_digit_tag", | |
| "timestamp_policy": "model_selected_nonnegative_number", | |
| "text_policy": "model_selected_nonempty_no_brackets", | |
| "eos_policy": "complete_segment_only", | |
| "vocab_size": self.vocab_size, | |
| "token_surface_sha256": self.token_surface_sha256, | |
| "eos_token_ids": sorted(self.eos_token_ids), | |
| } | |
| class TranscriptGrammarLogitsProcessor(LogitsProcessor): | |
| """Mask tokens that cannot extend the canonical transcript grammar.""" | |
| def __init__( | |
| self, | |
| tokenizer, | |
| prompt_length: int, | |
| *, | |
| vocabulary: TranscriptGrammarVocabulary | None = None, | |
| ): | |
| if prompt_length < 0: | |
| raise ValueError("prompt_length must be non-negative") | |
| self.prompt_length = int(prompt_length) | |
| self.vocabulary = vocabulary or TranscriptGrammarVocabulary(tokenizer) | |
| if self.vocabulary.tokenizer is not tokenizer: | |
| raise ValueError("constraint vocabulary belongs to another tokenizer instance") | |
| self.tokenizer = tokenizer | |
| self.vocab_size = self.vocabulary.vocab_size | |
| self.eos_token_ids = self.vocabulary.eos_token_ids | |
| def consume(self, token_ids: Iterable[int]) -> GrammarState: | |
| return self.vocabulary.consume(token_ids) | |
| def allowed_token_ids(self, state: GrammarState, device: torch.device) -> torch.Tensor: | |
| return self.vocabulary.allowed_token_ids(state, device) | |
| def audit(self) -> dict: | |
| return self.vocabulary.audit() | |
| def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor: | |
| if input_ids.ndim != 2 or scores.ndim != 2 or input_ids.shape[0] != scores.shape[0]: | |
| raise ValueError("transcript grammar requires aligned 2-D token and score batches") | |
| constrained = torch.full_like(scores, -torch.inf) | |
| # Hugging Face expands a batch-one prompt to one row per beam. Replay | |
| # each prefix independently so beam reordering cannot leak DFA state. | |
| for row in range(input_ids.shape[0]): | |
| generated = input_ids[row, self.prompt_length :].tolist() | |
| state = self.consume(generated) | |
| allowed = self.allowed_token_ids(state, scores.device) | |
| constrained[row].index_copy_(0, allowed, scores[row].index_select(0, allowed)) | |
| return constrained | |
| def generate_constrained_transcription( | |
| model, | |
| processor, | |
| messages, | |
| *, | |
| max_length: int = 131072, | |
| max_new_tokens: int, | |
| device: torch.device, | |
| dtype: torch.dtype, | |
| constraint_vocabulary: TranscriptGrammarVocabulary | None = None, | |
| num_beams: int = 1, | |
| ) -> dict: | |
| """Generate one transcript with syntax-only constraints.""" | |
| from moss_transcribe_diarize.inference_utils import prepare_inputs | |
| preprocess_context = ( | |
| torch.amp.autocast("cuda", dtype=dtype) | |
| if device.type == "cuda" and dtype in (torch.float16, torch.bfloat16) | |
| else torch.no_grad() | |
| ) | |
| with preprocess_context: | |
| inputs = prepare_inputs( | |
| processor, messages, max_length=max_length, device=device | |
| ).to(device) | |
| prompt_length = int(inputs["attention_mask"][0].sum().item()) | |
| constraint = TranscriptGrammarLogitsProcessor( | |
| processor.tokenizer, | |
| prompt_length, | |
| vocabulary=constraint_vocabulary, | |
| ) | |
| generation_config = copy.deepcopy(model.generation_config) | |
| generation_config.max_new_tokens = int(max_new_tokens) | |
| generation_config.do_sample = False | |
| generation_config.num_beams = int(num_beams) | |
| generation_config.num_return_sequences = 1 | |
| if num_beams > 1: | |
| generation_config.early_stopping = True | |
| generation_context = ( | |
| torch.amp.autocast("cuda", dtype=dtype) | |
| if device.type == "cuda" and dtype in (torch.float16, torch.bfloat16) | |
| else torch.no_grad() | |
| ) | |
| started = time.perf_counter() | |
| with torch.inference_mode(), generation_context: | |
| outputs = model.generate( | |
| input_ids=inputs["input_ids"], | |
| attention_mask=inputs["attention_mask"], | |
| input_features=inputs["input_features"], | |
| audio_feature_lengths=inputs["audio_feature_lengths"], | |
| audio_chunk_mapping=inputs["audio_chunk_mapping"], | |
| generation_config=generation_config, | |
| logits_processor=[constraint], | |
| ) | |
| if device.type == "cuda": | |
| torch.cuda.synchronize(device) | |
| generated_ids = outputs[0][prompt_length:] | |
| non_eos = [ | |
| int(token_id) | |
| for token_id in generated_ids.tolist() | |
| if int(token_id) not in constraint.eos_token_ids | |
| ] | |
| final_state = constraint.consume(non_eos) | |
| text = processor.tokenizer.decode( | |
| generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False | |
| ).strip() | |
| return { | |
| "text": text, | |
| "prompt_len": prompt_length, | |
| "generated_tokens": int(generated_ids.numel()), | |
| "inference_seconds": time.perf_counter() - started, | |
| "constraint_complete": accepting(final_state), | |
| "constraint_final_phase": final_state.phase, | |
| "constraint_num_beams": int(num_beams), | |
| "constraint_audit": constraint.audit(), | |
| } | |