Download code/tokenizer.py from maxpicy/mamba-cot-tts: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/maxpicy/mamba-cot-tts/resolve/main/code/tokenizer.py
- Command line
-
hf download hf://maxpicy/mamba-cot-tts/code/tokenizer.py
-
curl -L -o tokenizer.py https://huggingface.co/maxpicy/mamba-cot-tts/resolve/main/code/tokenizer.py
13.1 kB
| # tokenizer.py: Extends the GPT-NeoX tokenizer with the project's tokens; TokenRegistry gives O(1) id lookups and | |
| # the speech-token mask. | |
| import torch | |
| from typing import Dict, List, Optional, Set, Tuple | |
| from transformers import AutoTokenizer | |
| BASE_TOKENIZER_NAME = "EleutherAI/gpt-neox-20b" | |
| BOS_TOKEN = "<BOS>" | |
| EOS_TOKEN = "<EOS>" | |
| PAD_TOKEN = "<PAD>" | |
| CONTROL_TOKENS = [BOS_TOKEN, EOS_TOKEN, PAD_TOKEN] | |
| USER_PROMPT_TOKEN = "[USER_PROMPT]" | |
| THINK_START_TOKEN = "<THINK>" | |
| THINK_END_TOKEN = "</THINK>" | |
| COT_DELIMITER_TOKENS = [THINK_START_TOKEN, THINK_END_TOKEN] | |
| AUDIO_START_TOKEN = "<AUDIO>" | |
| AUDIO_END_TOKEN = "</AUDIO>" | |
| AUDIO_DELIMITER_TOKENS = [AUDIO_START_TOKEN, AUDIO_END_TOKEN] | |
| EMOTION_TAGS = [ | |
| "[EMO:neutral]", | |
| "[EMO:sarcastic]", | |
| "[EMO:angry]", | |
| "[EMO:sad]", | |
| "[EMO:joyful]", | |
| "[EMO:fearful]", | |
| "[EMO:ironic]", | |
| "[EMO:deadpan]", | |
| ] | |
| PROSODY_TAGS = [ | |
| "[PACE:slow]", | |
| "[PACE:normal]", | |
| "[PACE:fast]", | |
| "[PITCH:low]", | |
| "[PITCH:mid]", | |
| "[PITCH:high]", | |
| "[BREAK:short]", | |
| "[BREAK:long]", | |
| ] | |
| from config import NUM_SPEECH_TOKENS, SPEAKER_TOKEN | |
| SPEECH_TOKEN_PREFIX = "[SPEECH_" | |
| SPEECH_TOKENS = [f"[SPEECH_{i}]" for i in range(NUM_SPEECH_TOKENS)] | |
| from config import ( | |
| ENABLE_PLAN_TOKENS, | |
| ENERGY_BINS, | |
| DURATION_BINS, | |
| NUM_STYLE_TOKENS, | |
| PITCH_BINS, | |
| PLAN_END_TOKEN, | |
| PLAN_START_TOKEN, | |
| PLAN_WORD_SEP_TOKEN, | |
| get_plan_duration_tokens, | |
| get_plan_energy_tokens, | |
| get_plan_pitch_tokens, | |
| get_plan_tokens, | |
| get_style_tokens, | |
| ) | |
| PLAN_TOKENS: List[str] = get_plan_tokens() if ENABLE_PLAN_TOKENS else [] | |
| def get_all_new_tokens() -> List[str]: | |
| all_tokens = [] | |
| all_tokens.extend(CONTROL_TOKENS) | |
| all_tokens.append(USER_PROMPT_TOKEN) | |
| all_tokens.extend(COT_DELIMITER_TOKENS) | |
| all_tokens.extend(AUDIO_DELIMITER_TOKENS) | |
| all_tokens.append(SPEAKER_TOKEN) | |
| all_tokens.extend(EMOTION_TAGS) | |
| all_tokens.extend(PROSODY_TAGS) | |
| all_tokens.extend(PLAN_TOKENS) | |
| all_tokens.extend(SPEECH_TOKENS) | |
| return all_tokens | |
| def load_base_tokenizer() -> AutoTokenizer: | |
| return AutoTokenizer.from_pretrained(BASE_TOKENIZER_NAME) | |
| def expand_tokenizer(tokenizer) -> int: | |
| all_new = get_all_new_tokens() | |
| special_tokens_dict = { | |
| "bos_token": BOS_TOKEN, | |
| "eos_token": EOS_TOKEN, | |
| "pad_token": PAD_TOKEN, | |
| "additional_special_tokens": all_new, | |
| } | |
| num_added = tokenizer.add_special_tokens(special_tokens_dict) | |
| return num_added | |
| def _resolve_exact(tokenizer, token: str) -> Optional[int]: | |
| token_id = tokenizer.convert_tokens_to_ids(token) | |
| if not isinstance(token_id, int) or isinstance(token_id, bool): | |
| return None | |
| return token_id if tokenizer.convert_ids_to_tokens(token_id) == token else None | |
| class TokenRegistry: | |
| def __init__(self, tokenizer): | |
| self.tokenizer = tokenizer | |
| self.bos_id: int = tokenizer.convert_tokens_to_ids(BOS_TOKEN) | |
| self.eos_id: int = tokenizer.convert_tokens_to_ids(EOS_TOKEN) | |
| self.pad_id: int = tokenizer.convert_tokens_to_ids(PAD_TOKEN) | |
| self.user_prompt_id: int = tokenizer.convert_tokens_to_ids(USER_PROMPT_TOKEN) | |
| self.think_start_id: int = tokenizer.convert_tokens_to_ids(THINK_START_TOKEN) | |
| self.think_end_id: int = tokenizer.convert_tokens_to_ids(THINK_END_TOKEN) | |
| self.audio_start_id: int = tokenizer.convert_tokens_to_ids(AUDIO_START_TOKEN) | |
| self.audio_end_id: int = tokenizer.convert_tokens_to_ids(AUDIO_END_TOKEN) | |
| self.speaker_id: int = tokenizer.convert_tokens_to_ids(SPEAKER_TOKEN) | |
| self.emotion_ids: Set[int] = set( | |
| tokenizer.convert_tokens_to_ids(EMOTION_TAGS) | |
| ) | |
| self.prosody_ids: Set[int] = set( | |
| tokenizer.convert_tokens_to_ids(PROSODY_TAGS) | |
| ) | |
| self.speech_token_ids: Set[int] = set( | |
| tokenizer.convert_tokens_to_ids(SPEECH_TOKENS) | |
| ) | |
| speech_id_list = sorted(self.speech_token_ids) | |
| self.speech_token_id_min: int = speech_id_list[0] | |
| self.speech_token_id_max: int = speech_id_list[-1] | |
| assert self.speech_token_id_max - self.speech_token_id_min + 1 == NUM_SPEECH_TOKENS, ( | |
| f"Speech token IDs are not contiguous: " | |
| f"min={self.speech_token_id_min}, max={self.speech_token_id_max}, " | |
| f"expected range={NUM_SPEECH_TOKENS}" | |
| ) | |
| self._speech_value_to_id: Dict[int, int] = { | |
| i: tokenizer.convert_tokens_to_ids(f"[SPEECH_{i}]") | |
| for i in range(NUM_SPEECH_TOKENS) | |
| } | |
| self._speech_id_to_value: Dict[int, int] = { | |
| v: k for k, v in self._speech_value_to_id.items() | |
| } | |
| self._init_plan_ids(tokenizer) | |
| def _init_plan_ids(self, tokenizer) -> None: | |
| self.plan_enabled: bool = False | |
| self.plan_start_id: Optional[int] = None | |
| self.plan_end_id: Optional[int] = None | |
| self.plan_word_sep_id: Optional[int] = None | |
| self.pitch_ids: List[int] = [] | |
| self.duration_ids: List[int] = [] | |
| self.energy_ids: List[int] = [] | |
| self.style_ids: List[int] = [] | |
| self.plan_token_ids: Set[int] = set() | |
| self.plan_token_id_min: Optional[int] = None | |
| self.plan_token_id_max: Optional[int] = None | |
| self._plan_id_to_bin: Dict[int, Tuple[str, int]] = {} | |
| probe = _resolve_exact(tokenizer, PLAN_START_TOKEN) | |
| if not ENABLE_PLAN_TOKENS: | |
| if probe is not None: | |
| raise RuntimeError( | |
| f"Tokenizer contains {PLAN_START_TOKEN} but MVC_ENABLE_PLAN_TOKENS " | |
| f"is off in this process. The vocabulary and the code would " | |
| f"disagree about the P²-CoT plan block — export " | |
| f"MVC_ENABLE_PLAN_TOKENS=1 (and the same MVC_NUM_STYLE_TOKENS) " | |
| f"before importing config/tokenizer." | |
| ) | |
| return | |
| if probe is None: | |
| raise RuntimeError( | |
| f"MVC_ENABLE_PLAN_TOKENS=1 but this tokenizer has no " | |
| f"{PLAN_START_TOKEN} token. expand_tokenizer() must be called on " | |
| f"the tokenizer in a process where the gate is already on (the " | |
| f"token list is built at import time)." | |
| ) | |
| expected = get_plan_tokens() | |
| resolved: List[int] = [] | |
| missing: List[str] = [] | |
| for token in expected: | |
| token_id = _resolve_exact(tokenizer, token) | |
| if token_id is None: | |
| missing.append(token) | |
| else: | |
| resolved.append(token_id) | |
| if missing: | |
| raise RuntimeError( | |
| f"{len(missing)} P²-CoT plan token(s) missing from the tokenizer " | |
| f"(e.g. {missing[:4]}). The tokenizer was expanded with a different " | |
| f"PITCH/DURATION/ENERGY_BINS or MVC_NUM_STYLE_TOKENS than this " | |
| f"process defines ({PITCH_BINS}/{DURATION_BINS}/{ENERGY_BINS}/" | |
| f"{NUM_STYLE_TOKENS})." | |
| ) | |
| n_pitch, n_dur, n_energy = PITCH_BINS, DURATION_BINS, ENERGY_BINS | |
| self.plan_start_id, self.plan_end_id, self.plan_word_sep_id = resolved[:3] | |
| cursor = 3 | |
| self.pitch_ids = resolved[cursor:cursor + n_pitch]; cursor += n_pitch | |
| self.duration_ids = resolved[cursor:cursor + n_dur]; cursor += n_dur | |
| self.energy_ids = resolved[cursor:cursor + n_energy]; cursor += n_energy | |
| self.style_ids = resolved[cursor:cursor + NUM_STYLE_TOKENS] | |
| self.plan_token_ids = set(resolved) | |
| if len(self.plan_token_ids) != len(resolved): | |
| raise RuntimeError( | |
| "Plan token ids are not unique — two plan tokens resolved to the " | |
| "same id, which means the tokenizer collapsed them (check that " | |
| "expand_tokenizer() added them as additional_special_tokens)." | |
| ) | |
| self.plan_token_id_min = min(resolved) | |
| self.plan_token_id_max = max(resolved) | |
| if self.plan_token_id_max - self.plan_token_id_min + 1 != len(resolved): | |
| raise RuntimeError( | |
| f"Plan token ids are not contiguous: min={self.plan_token_id_min}, " | |
| f"max={self.plan_token_id_max}, count={len(resolved)}. The plan " | |
| f"block must be added in one run by expand_tokenizer()." | |
| ) | |
| if self.plan_token_id_max >= self.speech_token_id_min: | |
| raise RuntimeError( | |
| f"Plan block (ids {self.plan_token_id_min}..{self.plan_token_id_max}) " | |
| f"overlaps or follows the speech block (from " | |
| f"{self.speech_token_id_min}). Plan tokens must be added BEFORE " | |
| f"the speech tokens — see the ordering note in config.py." | |
| ) | |
| for family, ids in ( | |
| ("pitch", self.pitch_ids), | |
| ("duration", self.duration_ids), | |
| ("energy", self.energy_ids), | |
| ("style", self.style_ids), | |
| ): | |
| for bin_index, token_id in enumerate(ids): | |
| self._plan_id_to_bin[token_id] = (family, bin_index) | |
| self.plan_enabled = True | |
| def _require_plan(self) -> None: | |
| if not self.plan_enabled: | |
| raise RuntimeError( | |
| "P²-CoT plan tokens are not in this tokenizer (built with " | |
| "MVC_ENABLE_PLAN_TOKENS=0). Export MVC_ENABLE_PLAN_TOKENS=1 before " | |
| "importing config/tokenizer, or branch on `registry.plan_enabled` / " | |
| "`registry.plan_start_id is None`." | |
| ) | |
| def _as_bin_index(value, family: str, num_bins: int) -> int: | |
| if isinstance(value, (str, bytes)) or isinstance(value, bool): | |
| raise ValueError(f"{family} bin {value!r} is not an integer bin index") | |
| try: | |
| index = int(value) | |
| except (TypeError, ValueError): | |
| raise ValueError( | |
| f"{family} bin {value!r} is not an integer bin index" | |
| ) from None | |
| if index != value: | |
| raise ValueError( | |
| f"{family} bin {value!r} is not integral (would truncate to {index})" | |
| ) | |
| if not 0 <= index < num_bins: | |
| raise ValueError( | |
| f"{family} bin {index} out of range [0, {num_bins})" | |
| ) | |
| return index | |
| def _bin_to_id(self, ids: List[int], value, family: str, num_bins: int) -> int: | |
| self._require_plan() | |
| return ids[self._as_bin_index(value, family, num_bins)] | |
| def _id_to_bin(self, token_id: int, family: str) -> int: | |
| self._require_plan() | |
| entry = self._plan_id_to_bin.get(int(token_id)) | |
| if entry is None or entry[0] != family: | |
| raise ValueError( | |
| f"token id {token_id} is not a {family} plan token " | |
| f"(got {entry[0] if entry else 'non-plan token'})" | |
| ) | |
| return entry[1] | |
| def pitch_bin_to_id(self, bin_index) -> int: | |
| return self._bin_to_id(self.pitch_ids, bin_index, "pitch", PITCH_BINS) | |
| def id_to_pitch_bin(self, token_id: int) -> int: | |
| return self._id_to_bin(token_id, "pitch") | |
| def duration_bin_to_id(self, bin_index) -> int: | |
| return self._bin_to_id(self.duration_ids, bin_index, "duration", DURATION_BINS) | |
| def id_to_duration_bin(self, token_id: int) -> int: | |
| return self._id_to_bin(token_id, "duration") | |
| def energy_bin_to_id(self, bin_index) -> int: | |
| return self._bin_to_id(self.energy_ids, bin_index, "energy", ENERGY_BINS) | |
| def id_to_energy_bin(self, token_id: int) -> int: | |
| return self._id_to_bin(token_id, "energy") | |
| def style_index_to_id(self, index) -> int: | |
| return self._bin_to_id(self.style_ids, index, "style", NUM_STYLE_TOKENS) | |
| def id_to_style_index(self, token_id: int) -> int: | |
| return self._id_to_bin(token_id, "style") | |
| def is_plan_token(self, token_id: int) -> bool: | |
| if self.plan_token_id_min is None: | |
| return False | |
| return self.plan_token_id_min <= token_id <= self.plan_token_id_max | |
| def build_plan_mask(self, input_ids: torch.Tensor) -> torch.BoolTensor: | |
| if self.plan_token_id_min is None: | |
| return torch.zeros_like(input_ids, dtype=torch.bool) | |
| return (input_ids >= self.plan_token_id_min) & (input_ids <= self.plan_token_id_max) | |
| def is_speech_token(self, token_id: int) -> bool: | |
| return self.speech_token_id_min <= token_id <= self.speech_token_id_max | |
| def is_emotion_token(self, token_id: int) -> bool: | |
| return token_id in self.emotion_ids | |
| def is_prosody_token(self, token_id: int) -> bool: | |
| return token_id in self.prosody_ids | |
| def speech_value_to_id(self, value: int) -> int: | |
| return self._speech_value_to_id[value] | |
| def speech_id_to_value(self, token_id: int) -> int: | |
| return self._speech_id_to_value[token_id] | |
| def build_speech_mask(self, input_ids: torch.Tensor) -> torch.BoolTensor: | |
| return (input_ids >= self.speech_token_id_min) & (input_ids <= self.speech_token_id_max) | |