File size: 10,135 Bytes
2a076e4 | 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 | from collections import deque
from dataclasses import dataclass
from typing import Deque, Dict, Iterator, List, Tuple
import numpy as np
import torch
from moshi.models import MimiModel
from voxtream.config import SpeechGeneratorConfig
from voxtream.utils.generator.context import FrameState, GenerationContext
from voxtream.utils.generator.helpers import interpolate_speaking_rate_params
@dataclass
class SpeakingRateRuntimeState:
cfg_gamma: float | None
spk_rate_weight: float | None = None
target_spk_rate_cnt: torch.Tensor | None = None
cur_spk_rate_cnt: torch.Tensor | None = None
spk_rate_window_frames: int | None = None
spk_rate_history: Deque[int] | None = None
last_speaking_rate: float | None = None
def decode_audio_frame(
mimi: MimiModel,
frame: torch.Tensor,
sem_code: torch.Tensor,
mimi_vocab_size: int,
) -> Tuple[np.ndarray, torch.Tensor]:
"""Decode predicted frame into audio."""
audio_frame = torch.cat([sem_code, frame[:, 1:]], dim=1)
audio_frame = torch.clamp(audio_frame, 0, int(mimi_vocab_size - 1)).to(torch.int64)
sem_code = frame[:, :1]
audio_frame = mimi.decode(audio_frame.unsqueeze(-1)).squeeze()
audio_frame = audio_frame.to(dtype=torch.float32).cpu().numpy()
return audio_frame, sem_code
_DWELL_CAPS = None
def _dwell_cap(ctx: GenerationContext, frame_state: FrameState, start: int) -> int:
"""v10.1: лимит удержания фонемы по классу. Единый frame_repeat_counter=25
(2 с) давал «Потомммм»/«добаввввил» — модель залипала на последней согласной
фразы вместо ухода в паузу (в данных 94% согласных перед знаком <= 3 кадра).
VOXTREAM_DWELL_CAPS="cons=6,vow=12" (кадры); sil/спец — прежний лимит."""
global _DWELL_CAPS
if _DWELL_CAPS is None:
import os
spec = os.environ.get("VOXTREAM_DWELL_CAPS", "")
caps = {}
for kv in spec.split(","):
if "=" in kv:
k, v = kv.split("=")
caps[k.strip()] = int(v)
_DWELL_CAPS = caps
pc = frame_state.phone_class
if not _DWELL_CAPS or pc is None or start >= len(pc):
return ctx.config.frame_repeat_counter
cls = int(pc[start])
if cls == 2 and "cons" in _DWELL_CAPS:
return _DWELL_CAPS["cons"]
if cls == 1 and "vow" in _DWELL_CAPS:
return _DWELL_CAPS["vow"]
return ctx.config.frame_repeat_counter
def update_indices_and_tokens(
pred_shift: torch.Tensor,
frame: torch.Tensor,
idx: int,
phone_seq_len: int,
frame_state: FrameState,
ctx: GenerationContext,
) -> Tuple[torch.Tensor, FrameState]:
"""Update phone embedding indices, audio tokens, and EOS logic."""
pred_shift_int = int(pred_shift.item())
shift, num_tokens = ctx.phoneme_index_map[str(pred_shift_int)]
start = frame_state.phone_emb_max_idx + shift
state = (start, start + num_tokens)
if state in frame_state.state_counter:
if start >= phone_seq_len - 2 and frame_state.state_counter[state] == 3:
start += 1
state = (start, start + num_tokens)
# Push the model to move forward if it's stuck on the same state for too long
elif frame_state.state_counter[state] > ctx.config.frame_repeat_counter:
start += 1
state = (start, start + num_tokens)
# v10.1: классовый лимит удержания ПО ПОЗИЦИИ фонемы (state_counter ключуется
# ещё и числом фонем в кадре — модель чередует 1/2 и порог расщепляется)
dwell_n = frame_state.dwell_n + 1 if start == frame_state.dwell_start else 1
if dwell_n > _dwell_cap(ctx, frame_state, start) and start < phone_seq_len:
start += 1
state = (start, start + num_tokens)
dwell_n = 1
eos_idx = frame_state.eos_idx
if start >= phone_seq_len:
end_token = min(start, phone_seq_len + 1)
val = [end_token] * ctx.config.num_phones_per_frame
eos_idx = idx
else:
val = list(range(int(start), int(start + num_tokens)))
while len(val) < ctx.config.num_phones_per_frame:
val.append(val[-1])
phone_emb_max_idx = val[-1]
phone_emb_indices = torch.tensor(
[[val]] * ctx.batch_size,
device=frame_state.phone_emb_indices.device,
dtype=torch.int64,
)
mimi_codes = frame.unsqueeze(dim=2).repeat((ctx.batch_size, 1, 1))
if state not in frame_state.state_counter:
frame_state.state_counter[state] = 1
else:
frame_state.state_counter[state] += 1
return (
mimi_codes,
FrameState(
phone_emb_indices=phone_emb_indices,
phone_emb_max_idx=phone_emb_max_idx,
eos_idx=eos_idx,
state_counter=frame_state.state_counter,
phone_class=frame_state.phone_class,
dwell_start=start,
dwell_n=dwell_n,
),
)
def init_spk_rate_state(
config: SpeechGeneratorConfig,
target_spk_rate_cnt: List[int] | None,
device: str,
) -> Tuple[
torch.Tensor | None,
torch.Tensor | None,
int | None,
Deque[int] | None,
]:
if target_spk_rate_cnt is None:
return None, None, None, None
target_spk_rate_cnt = torch.tensor(
target_spk_rate_cnt,
dtype=torch.int64,
device=device,
)
cur_spk_rate_cnt = torch.ones_like(target_spk_rate_cnt)
frames = (
max(
1,
int(round(config.spk_rate_window_sec * 1000 / config.mimi_frame_ms)),
)
if config.spk_rate_window_sec is not None and config.spk_rate_window_sec > 0
else None
)
return (
target_spk_rate_cnt,
cur_spk_rate_cnt,
frames,
deque() if frames else None,
)
def init_current_duration_state(
config: SpeechGeneratorConfig,
device: str,
) -> Tuple[torch.Tensor, int | None, Deque[int] | None]:
duration_bins = max(int(key) for key in config.phoneme_index_map) + 1
cur_spk_rate_cnt = torch.ones(
duration_bins,
dtype=torch.int64,
device=device,
)
frames = (
max(
1,
int(round(config.spk_rate_window_sec * 1000 / config.mimi_frame_ms)),
)
if config.spk_rate_window_sec is not None and config.spk_rate_window_sec > 0
else None
)
return cur_spk_rate_cnt, frames, deque() if frames else None
def update_speaking_rate_params(
speaking_rate: Iterator[float] | None,
speaking_rate_config: Dict[str, Dict[str, list | float]],
state: SpeakingRateRuntimeState,
config: SpeechGeneratorConfig,
device: str,
logger=None,
) -> SpeakingRateRuntimeState:
if speaking_rate is None or speaking_rate_config is None:
return state
try:
current_speaking_rate = float(next(speaking_rate))
except StopIteration as exc:
raise ValueError(
"speaking_rate generator must yield indefinitely. "
"For a fixed speaking rate, use an iterator that repeats one value."
) from exc
if current_speaking_rate == state.last_speaking_rate:
return state
duration_state, state.spk_rate_weight, state.cfg_gamma = (
interpolate_speaking_rate_params(
speaking_rate_config,
current_speaking_rate,
logger=logger,
)
)
if state.target_spk_rate_cnt is None:
(
state.target_spk_rate_cnt,
state.cur_spk_rate_cnt,
state.spk_rate_window_frames,
state.spk_rate_history,
) = init_spk_rate_state(
config=config,
target_spk_rate_cnt=duration_state,
device=device,
)
else:
updated_target_spk_rate_cnt = torch.tensor(
duration_state,
dtype=torch.int64,
device=device,
)
if updated_target_spk_rate_cnt.shape == state.target_spk_rate_cnt.shape:
state.target_spk_rate_cnt = updated_target_spk_rate_cnt
else:
(
state.target_spk_rate_cnt,
state.cur_spk_rate_cnt,
state.spk_rate_window_frames,
state.spk_rate_history,
) = init_spk_rate_state(
config=config,
target_spk_rate_cnt=duration_state,
device=device,
)
state.last_speaking_rate = current_speaking_rate
return state
def update_speaking_rate_history(
state: SpeakingRateRuntimeState,
pred_shift: torch.Tensor,
) -> SpeakingRateRuntimeState:
if state.spk_rate_history is None or state.cur_spk_rate_cnt is None:
return state
state.spk_rate_history.append(int(pred_shift.item()))
if len(state.spk_rate_history) > state.spk_rate_window_frames:
dropped = state.spk_rate_history.popleft()
state.cur_spk_rate_cnt[dropped] -= 1
return state
def progress_metadata(
generated_audio_frames: int,
audio_frame_sec: float,
frame_state: FrameState,
prompt_phone_end_idx: int,
speaking_rate_enabled: bool,
speaking_rate_state: SpeakingRateRuntimeState,
) -> Dict:
def counter_to_list(counter):
if counter is None:
return None
if isinstance(counter, torch.Tensor):
return counter.detach().float().cpu().reshape(-1).tolist()
return list(counter)
return {
"time_sec": generated_audio_frames * audio_frame_sec,
"phone_position": max(
0, int(frame_state.phone_emb_max_idx - prompt_phone_end_idx)
),
"speaking_rate": (
speaking_rate_state.last_speaking_rate if speaking_rate_enabled else None
),
"target_duration_state": counter_to_list(
speaking_rate_state.target_spk_rate_cnt
),
"current_duration_state": counter_to_list(speaking_rate_state.cur_spk_rate_cnt),
}
|