File size: 13,069 Bytes
6e802a4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | # 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`."
)
@staticmethod
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)
|