from monotonic_align import maximum_path from monotonic_align import mask_from_lens from monotonic_align.core import maximum_path_c import numpy as np import torch import copy from torch import nn import torch.nn.functional as F import torchaudio import librosa import matplotlib.pyplot as plt from munch import Munch from typing import List, Tuple, Optional, Union import random import re import math def maximum_path(neg_cent, mask): """ Cython optimized version. neg_cent: [b, t_t, t_s] mask: [b, t_t, t_s] """ device = neg_cent.device dtype = neg_cent.dtype neg_cent = np.ascontiguousarray(neg_cent.data.cpu().numpy().astype(np.float32)) path = np.ascontiguousarray(np.zeros(neg_cent.shape, dtype=np.int32)) t_t_max = np.ascontiguousarray(mask.sum(1)[:, 0].data.cpu().numpy().astype(np.int32)) t_s_max = np.ascontiguousarray(mask.sum(2)[:, 0].data.cpu().numpy().astype(np.int32)) maximum_path_c(path, neg_cent, t_t_max, t_s_max) return torch.from_numpy(path).to(device=device, dtype=dtype) def get_data_path_list(train_path=None, val_path=None): if train_path is None: train_path = "Data/train_list.txt" if val_path is None: val_path = "Data/val_list.txt" with open(train_path, 'r', encoding='utf-8', errors='ignore') as f: train_list = f.readlines() with open(val_path, 'r', encoding='utf-8', errors='ignore') as f: val_list = f.readlines() return train_list, val_list def length_to_mask(lengths, max_len=None): if max_len is None: max_len = lengths.max() mask = torch.arange(max_len).unsqueeze(0).expand(lengths.shape[0], -1).type_as(lengths) mask = torch.gt(mask+1, lengths.unsqueeze(1)) return mask # for norm consistency loss def log_norm1(x, mean=-4, std=4, dim=2): """ normalized log mel -> mel -> norm -> log(norm) """ x = torch.log(torch.exp(x * std + mean).norm(dim=dim)) return x def log_norm(x, mean=-4.0, std=4.0, dim=2): """ normalized log-mel x -> de-norm log-mel z -> log(sum exp(z)) over freq Returns a per-frame scalar along `dim`. """ z = x * std + mean return torch.logsumexp(z, dim=dim) / math.log(10.0) def get_image(arrs): plt.switch_backend('agg') fig = plt.figure() ax = plt.gca() ax.imshow(arrs) return fig def recursive_munch(d): if isinstance(d, dict): return Munch((k, recursive_munch(v)) for k, v in d.items()) elif isinstance(d, list): return [recursive_munch(v) for v in d] else: return d def log_print(message, logger): logger.info(message) print(message) def build_context_window_center_exists_prob( sample: Union[List[str], Tuple[str, ...], str], num_context: int = 5, drop_prob: float = 0.05, rng: Optional[np.random.RandomState] = None, return_mask: bool = True, ) -> Union[Tuple[List[str], List[bool]], List[str]]: """ Build a symmetric context window around the center with hierarchical drops: - With probability drop_prob: choose side (50/50) and neighbor (1 or 2). * If neighbor-1 chosen -> drop neighbor-1 AND neighbor-2 on that side. * If neighbor-2 chosen -> drop only neighbor-2 on that side. Center is NEVER dropped and is always marked True in the mask. Returns (window_list, orig_mask) when return_mask is True. """ if rng is None: rng = np.random # Single string -> repeat, all True if not isinstance(sample, (list, tuple)): window = [str(sample)] * num_context return (window, [True] * num_context) if return_mask else window L = len(sample) if L == 0: window = [""] * num_context return (window, [False] * num_context) if return_mask else window if num_context % 2 == 0: raise ValueError("num_context must be odd (e.g. 5)") center_idx = L // 2 # user guarantee: center exists half = num_context // 2 slot_sample_idxs = [center_idx - half + i for i in range(num_context)] # initial fill: strings or None if out-of-bounds values: List[Optional[str]] = [] src_idx: List[Optional[int]] = [] for idx in slot_sample_idxs: if 0 <= idx < L: values.append(sample[idx]) src_idx.append(idx) else: values.append(None) src_idx.append(None) center_pos = half # ensure center is set and marked original if values[center_pos] is None: values[center_pos] = sample[center_idx] src_idx[center_pos] = center_idx # Decide hierarchical drop once per sample if rng.random() < drop_prob: go_left = (rng.random() < 0.5) choose_neighbor1 = (rng.random() < 0.5) prev1_pos = center_pos - 1 prev2_pos = center_pos - 2 next1_pos = center_pos + 1 next2_pos = center_pos + 2 if go_left: if choose_neighbor1: if 0 <= prev1_pos < num_context: values[prev1_pos] = None; src_idx[prev1_pos] = None if 0 <= prev2_pos < num_context: values[prev2_pos] = None; src_idx[prev2_pos] = None else: if 0 <= prev2_pos < num_context: values[prev2_pos] = None; src_idx[prev2_pos] = None else: if choose_neighbor1: if 0 <= next1_pos < num_context: values[next1_pos] = None; src_idx[next1_pos] = None if 0 <= next2_pos < num_context: values[next2_pos] = None; src_idx[next2_pos] = None else: if 0 <= next2_pos < num_context: values[next2_pos] = None; src_idx[next2_pos] = None # Fill missing slots with fallback order: center -> next outward -> prev outward def fill_missing_slot(i: int): if values[i] not in (None, ""): return cand_positions = [center_pos] for d in range(1, num_context): p = center_pos + d if p < num_context: cand_positions.append(p) for d in range(1, num_context): p = center_pos - d if p >= 0: cand_positions.append(p) for p in cand_positions: if values[p] not in (None, ""): values[i] = values[p] return values[i] = "" for i in range(num_context): if i == center_pos: continue fill_missing_slot(i) final_window = [str(v) for v in values] if return_mask: original_mask = [si is not None for si in src_idx] # ensure center mask is True (enforce) original_mask[center_pos] = True return final_window, original_mask return final_window def mask_input_ids_random( input_ids, attention_mask, mask_token_id, p_mask=0.15, exclude_token_ids=None, seed=None, ): """ Losowo zamienia tokeny na mask_token_id z prawdopodobienstwem p_mask. Nie dotyka paddingu (attention_mask==0), ani tokenów z exclude_token_ids. input_ids: Tensor [B, L] attention_mask: Tensor [B, L] Returns: new_input_ids (cloned) """ if seed is not None: torch.manual_seed(seed) random.seed(seed) B, L = input_ids.shape new_input_ids = input_ids.clone() if exclude_token_ids is None: exclude_token_ids = set() for i in range(B): valid_mask = attention_mask[i].bool() # indices we can consider (True where mask==1 and token not excluded) cand_idx = [] for j in range(L): if not valid_mask[j]: continue tok = int(input_ids[i, j].item()) if tok in exclude_token_ids: continue cand_idx.append(j) if not cand_idx: continue # sample positions independently probs = torch.rand(len(cand_idx)) for k, pos in enumerate(cand_idx): if probs[k].item() < p_mask: new_input_ids[i, pos] = mask_token_id return new_input_ids def shuffle_segments_by_separator( input_ids, attention_mask, sep_token_id, p_shuffle=0.2, bos_token_id=None, eos_token_id=None, seed=None, ): """ Dzieli sekwencję na segmenty rozdzielone sep_token_id (w obrębie ważnej części: od pierwsnego do ostatniego tokena gdzie attention_mask==1), następnie z prawdopodobieństwem p_shuffle permutuje kolejność segmentów. BOS i EOS są zachowane na swoich pozycjach. input_ids: Tensor [B, L] Returns: new_input_ids (cloned) """ if seed is not None: random.seed(seed) B, L = input_ids.shape new_input_ids = input_ids.clone() pad_token_id = 0 # fallback, można pobrać z tokenizer.pad_token_id jeśli dostępne for i in range(B): valid_positions = (attention_mask[i] == 1).nonzero(as_tuple=True)[0].tolist() if not valid_positions: continue first_pos = valid_positions[0] last_pos = valid_positions[-1] # verify first is bos and last is eos when given (if not given, still proceed) seq = input_ids[i].tolist() # We will keep seq[first_pos] and seq[last_pos] unchanged. middle = seq[first_pos + 1 : last_pos] # list if not middle: continue # split middle into chunks where sep_token ends chunk chunks = [] current = [] for tok in middle: current.append(tok) if tok == sep_token_id: chunks.append(current) current = [] if current: chunks.append(current) # if there is only one chunk or no sep present, nothing to shuffle if len(chunks) <= 1: continue if random.random() < p_shuffle: random.shuffle(chunks) new_middle = [] for c in chunks: new_middle.extend(c) # build new sequence keeping first_pos and last_pos tokens new_seq = [] # tokens before first_pos (likely BOS and maybe specials) # keep prefix up to first_pos (inclusive) new_seq.extend(seq[: first_pos + 1]) new_seq.extend(new_middle) # then last token(s) from last_pos to end (inclusive) new_seq.extend(seq[last_pos:]) # pad / truncate to L if len(new_seq) < L: new_seq = new_seq + [pad_token_id] * (L - len(new_seq)) else: new_seq = new_seq[:L] new_input_ids[i] = torch.tensor(new_seq, dtype=input_ids.dtype, device=input_ids.device) return new_input_ids # --- UTILS: embedding-level augmentations (po gemma_mrl) --- def augment_embeddings_noise_and_dropout( embeddings, attention_mask=None, # optional: [B, T] mask aligned with embeddings if available p_noise=0.1, noise_std=0.02, p_dropout=0.05, seed=None, ): """ embeddings: Tensor [B, T, D] (np. output gemma_mrl) Dodaje gaussowski szum do losowo wybranych embeddingów i/lub ustawia niektóre embeddingi na zero (dropout). """ if seed is not None: torch.manual_seed(seed) random.seed(seed) B, T, D = embeddings.shape out = embeddings.clone() # noise if p_noise > 0: mask = torch.rand(B, T, device=embeddings.device) < p_noise if attention_mask is not None: # attention_mask: align shape [B, T] (optional) mask = mask & (attention_mask.bool()) noise = torch.randn_like(out) * noise_std out = out + noise * mask.unsqueeze(-1).to(out.dtype) # dropout (zero-out some embeddings) if p_dropout > 0: drop_mask = torch.rand(B, T, device=embeddings.device) < p_dropout if attention_mask is not None: drop_mask = drop_mask & (attention_mask.bool()) out[drop_mask] = 0.0 return out class DurationProcessor(torch.nn.Module): def __init__(self, class_count, max_dur): super(DurationProcessor, self).__init__() self.class_count = class_count self.max_dur = max_dur class_to_dur_table = torch.Tensor( [1, 2, 3, 4, 5, 6, 7, 9, 12, 15, 18, 22, 27, 32, 38, 46] ) self.register_buffer("class_to_dur_table", class_to_dur_table) dur_to_class_table = torch.Tensor( [ 0, 0, 1, 2, 3, 4, 5, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, ] ) self.register_buffer("dur_to_class_table", dur_to_class_table) # def class_to_dur_soft(self, class_dist): # return class_dist * self.class_to_dur_table def class_to_dur_soft(self, softdur): result = (softdur * self.class_to_dur_table).sum(dim=-1) / ( softdur.sum(dim=-1) + 1e-9 ) return result def class_to_dur_hard(self, classes): classes = classes.clamp(min=0, max=self.class_count) return self.class_to_dur_table[classes] def dur_to_class(self, durs): durs = durs.clamp(min=1, max=self.max_dur) return self.dur_to_class_table[durs.long()] def align_to_class(self, alignment): result = alignment.sum(dim=-1).clamp(min=1, max=50) result = self.dur_to_class(result) return result def prediction_to_duration(self, pred, text_length): # softdur = self.class_to_dur_soft(torch.softmax(pred, dim=-1)) # softdur = softdur.sum(dim=-1).round().clamp(min=1) # argmax = torch.argmax(pred, dim=-1).long() # argdur = self.class_to_dur_hard(argmax) confidence = torch.softmax(pred, dim=-1) softdur = self.class_to_dur_soft(confidence) dur = softdur # dur = (argdur * (argdur < 7)) + (softdur * (argdur >= 7)) # dur = dur[:text_length] return dur @torch.no_grad() def _make_masks( self,B: int, S: int, T: int, text_lens: Optional[torch.Tensor], mel_lens: torch.Tensor ): device = mel_lens.device s_idx = torch.arange(S, device=device).view(1, S) t_idx = torch.arange(T, device=device).view(1, T) if text_lens is None: token_mask = torch.ones(B, S, dtype=torch.bool, device=device) else: token_mask = s_idx < text_lens.view(B, 1) frame_mask = t_idx < mel_lens.view(B, 1) return token_mask, frame_mask def align_from_softdurations( self, d_gt: torch.Tensor, # [B, S], nonnegative; trailing zeros = pads mel_lens: Optional[torch.Tensor] = None, # [B], ints (#frames per sample) tau: float = 1.0, # entropic temperature (smaller = sharper) sigma_scale: float = 0.5, # window width grows with duration sigma_floor: float = 0.5, # minimum width iters: int = 60, # Sinkhorn iterations eps: float = 1e-8, ) -> torch.Tensor: """ Reconstruct a soft alignment P from soft durations d_gt using entropic OT. Args: d_gt: [B, S] nonnegative durations (can be fractional). Zeros are treated as padded tokens if text lengths are unknown. mel_lens: [B] number of frames per sample. If None, it's inferred as round(sum(d_gt[b])) for each sample. tau: entropic regularization temperature (lower = crisper). sigma_scale, sigma_floor: shape the Gaussian cost (not the row mass). iters: Sinkhorn iterations. eps: small epsilon for numerical stability. Returns: P: [B, S, T_max] alignment. For each sample b: - sum_t P[b, i, t] == scaled d_gt[b, i] - sum_i P[b, i, t] == 1 for t < mel_lens[b], else 0 """ assert d_gt.dim() == 2 B, S = d_gt.shape device, dtype = d_gt.device, d_gt.dtype # Infer mel lengths if not provided: T_b ≈ sum of durations for that sample. if mel_lens is None: mel_lens = ( d_gt.sum(dim=1).round().clamp_min(1).to(torch.long) ) # [B] T = int(mel_lens.max().item()) # Masks token_mask = d_gt > eps # [B, S] treat zeros as padded tokens frame_idx = torch.arange(T, device=device).view(1, T) frame_mask = frame_idx < mel_lens.view(B, 1) # [B, T] token_mask3 = token_mask.unsqueeze(-1) # [B, S, 1] frame_mask3 = frame_mask.unsqueeze(1) # [B, 1, T] # Target row marginals r: scale d_gt to match mel_lens exactly per sample. d = torch.where(token_mask, d_gt, torch.zeros_like(d_gt)) # zero-out pads sum_d = d.sum(dim=1, keepdim=True).clamp_min(eps) # [B, 1] scale = mel_lens.to(dtype).view(B, 1) / sum_d # [B, 1] r = d * scale # [B, S] # Gaussian-shaped cost centered by cumulative r (monotonic prior). end = torch.cumsum(r, dim=1) # [B, S] centers = (end - 0.5 * r).unsqueeze(-1) # [B, S, 1] sigma = sigma_scale * r.unsqueeze(-1) + sigma_floor # [B, S, 1] t = torch.arange(T, device=device, dtype=dtype).view(1, 1, T) x = t - centers cost = 0.5 * (x / (sigma + eps)) ** 2 # [B, S, T] big = 1e6 cost = cost + (~token_mask3) * big + (~frame_mask3) * big logK = -cost / tau # kernel in log-space # Log-marginals log_r = torch.full((B, S), -float("inf"), device=device, dtype=dtype) valid_r = r > 0 log_r[valid_r] = torch.log(r[valid_r]) log_c = torch.full((B, T), -float("inf"), device=device, dtype=dtype) log_c[frame_mask] = 0.0 # log(1) for valid frames # Sinkhorn iterations (log-domain) log_u = torch.zeros((B, S), device=device, dtype=dtype) log_v = torch.zeros((B, T), device=device, dtype=dtype) for _ in range(iters): logMv = torch.logsumexp(logK + log_v.unsqueeze(1), dim=2) # [B, S] log_u = log_r - logMv logKu = torch.logsumexp(logK + log_u.unsqueeze(2), dim=1) # [B, T] log_v = log_c - logKu logP = log_u.unsqueeze(2) + logK + log_v.unsqueeze(1) # [B, S, T] P = torch.exp(logP) * token_mask3 * frame_mask3 return P def duration_to_alignment_sinkhorn_batched( self, durations: torch.Tensor, # [B, S], nonnegative mel_lens: torch.Tensor, # [B], ints (#frames per sample) text_lens: Optional[torch.Tensor] = None, # [B], ints (valid tokens) tau: float = 1.0, # entropic temperature (smaller = sharper) sigma_scale: float = 0.5, # width grows with duration sigma_floor: float = 0.5, # minimum width iters: int = 60, # Sinkhorn iterations eps: float = 1e-8, ) -> torch.Tensor: """ Returns P in [B, S, T_max] s.t. - sum_t P[b, i, t] == r[b, i] (scaled durations) - sum_i P[b, i, t] == 1 for t < mel_lens[b], else 0 Fully differentiable w.r.t. durations. """ assert durations.dim() == 2 B, S = durations.shape device = durations.device dtype = durations.dtype T = int(mel_lens.max().item()) # Masks token_mask, frame_mask = self._make_masks(B, S, T, text_lens, mel_lens) token_mask3 = token_mask.unsqueeze(-1) # [B, S, 1] frame_mask3 = frame_mask.unsqueeze(1) # [B, 1, T] # Build row marginals r: scale durations to sum to mel_lens per sample. d = durations.clamp_min(eps) if text_lens is not None: d = d * token_mask.float() sum_d = d.sum(dim=1, keepdim=True).clamp_min(eps) # [B, 1] scale = mel_lens.to(dtype).view(B, 1) / sum_d # [B, 1] r = d * scale # [B, S], sums to mel_len per sample on valid tokens # Zero-out padded tokens exactly in r if text_lens is not None: r = r * token_mask.float() # Gaussian-shaped cost around differentiable centers from r end = torch.cumsum(r, dim=1) # [B, S] centers = (end - 0.5 * r).unsqueeze(-1) # [B, S, 1] sigma = sigma_scale * r.unsqueeze(-1) + sigma_floor # [B, S, 1] t = torch.arange(T, device=device, dtype=dtype).view(1, 1, T) x = t - centers # Cost ~ 0.5 * (x/sigma)^2 (no absolute scale needed; tau controls sharpness) cost = 0.5 * (x / (sigma + eps)) ** 2 # [B, S, T] big = 1e6 if text_lens is not None: cost = cost + (~token_mask3) * big cost = cost + (~frame_mask3) * big logK = -cost / tau # [B, S, T] # Log marginals log_r = torch.full((B, S), -float("inf"), device=device, dtype=dtype) valid_r = r > 0 log_r[valid_r] = torch.log(r[valid_r]) log_c = torch.full((B, T), -float("inf"), device=device, dtype=dtype) log_c[frame_mask] = 0.0 # log(1) for valid frames # Sinkhorn iterations in log-domain log_u = torch.zeros((B, S), device=device, dtype=dtype) log_v = torch.zeros((B, T), device=device, dtype=dtype) for _ in range(iters): # log_u = log_r - logsumexp_j [logK + log_v_j] logMv = torch.logsumexp(logK + log_v.unsqueeze(1), dim=2) # [B, S] log_u = log_r - logMv # log_v = log_c - logsumexp_i [logK + log_u_i] logKu = torch.logsumexp(logK + log_u.unsqueeze(2), dim=1) # [B, T] log_v = log_c - logKu # Transport plan logP = log_u.unsqueeze(2) + logK + log_v.unsqueeze(1) # [B, S, T] P = torch.exp(logP) # Mask out pads for cleanliness (mass is already ~0 there) P = P * token_mask3 * frame_mask3 return P def duration_to_alignment_soft( self, duration: torch.Tensor, width_factor: float =1.0, min_width: float = 1.0, mask_margin: int = 2, temperature: float = 0.5, ) -> torch.Tensor: """ Convert durations -> attention matrix. duration: (T,) or (B, T) of positive frame counts (floats or ints). Returns: (T, A) if input was (T,), else (B, T, A). T = text length, A = audio frames (sum of durations). width_factor controls the per-token sigma ~ duration * width_factor. min_width prevents sigma from becoming too small. temperature < 1 sharpens the softmax (use 1.0 for no change). mask_margin extends the token window by this many frames. """ single_example = False if duration.dim() == 1: duration = duration.unsqueeze(0) # -> (1, T) single_example = True device = duration.device duration = duration.to(dtype=torch.float32, device=device) # total audio length (A) total_dur = int(duration.sum(dim=1).round().max().item()) if total_dur <= 0: total_dur = 1 # per-token cumulative bounds and centers upper = torch.cumsum(duration, dim=1) # (B, T) lower = upper - duration mean = (lower + upper) / 2.0 # (B, T) mean = mean.unsqueeze(2) # (B, T, 1) # frame indices (1, 1, A) seq = torch.arange(total_dur, device=device, dtype=duration.dtype) seq = seq.unsqueeze(0).unsqueeze(0) # (1, 1, A) # distance from center (B, T, A) x = seq - mean # per-token sigma (B, T, 1) sigma = (duration.unsqueeze(2) * width_factor) + min_width sigma = sigma.clamp(min=1e-6) # logits = -0.5 * (x / sigma)**2 (Gaussian log-likelihood up to const) logits = -0.5 * (x / sigma) ** 2 # mask out-of-bound frames by setting logits to a very large negative value lower_mask = seq >= (lower - mask_margin).unsqueeze(2) upper_mask = seq <= (upper + mask_margin).unsqueeze(2) mask = (lower_mask & upper_mask).to(dtype=torch.bool) logits = logits.masked_fill(~mask, -1e9) # optional sharpening via temperature: smaller -> sharper if temperature <= 0: raise ValueError("temperature must be > 0") logits = logits / float(temperature) # softmax over text positions (dim=1) so each audio frame has a distribution over tokens alignment = torch.softmax(logits, dim=1) if single_example: return alignment[0] # (T, A) return alignment # (B, T, A) def duration_to_alignment(self, duration: torch.Tensor) -> torch.Tensor: indices = torch.repeat_interleave( torch.arange(duration.shape[0], device=duration.device), duration.to(torch.int), ) result = torch.zeros( (duration.shape[0], indices.shape[0]), device=duration.device ) result[indices, torch.arange(indices.shape[0])] = 1 return result def forward(self, pred, text_length): duration = self.prediction_to_duration(pred, text_length) alignment = self.duration_to_alignment_soft(duration) return 0 def torch_empty_cache(device): if device == "cuda": torch.cuda.synchronize() torch.cuda.empty_cache() elif device == "mps": torch.mps.synchronize() torch.mps.empty_cache() elif device == "cpu": # torch.cpu.synchronize() # torch.cpu.empty_cache() pass else: exit(f"Unknown device {device}. Could not empty cache.") SENTENCE_SPLIT_RE = re.compile(r'(?<=[\.\?\!])\s+|\n+') def split_sentences(text: Optional[str]) -> List[str]: """Dzieli tekst na zdania; jeśli brak typowych końców zdań, traktuje cały tekst jako jedno zdanie. Zwraca listę zdań.""" if text is None: return [] text = text.strip() if not text: return [] parts = [s.strip() for s in SENTENCE_SPLIT_RE.split(text) if s.strip()] if not parts: return [text] return parts def select_context(prev2_txt: str, prev1_txt: str, curr_txt: str, next_txt: str) -> List[str]: """Zwraca listę zdań do użycia jako embeddingi zgodnie z regułami: - jeśli curr >=4 -> wszystkie zdania z curr - jeśli curr ==3 -> prev2=curr[0], prev1=curr[1], curr=curr[2], next=first(next) - jeśli curr ==2 -> prev2=last(prev1) (fallback z prev2), prev1=curr[0], curr=curr[1], next=last(next) - jeśli curr ==1 -> prev1/prev2 zgodnie z regułami opisanymi przez Ciebie, next=first(next) - jeśli curr ==0 -> fallback: ostatnie 4 zdania z (prev2+prev1+next) lub puste stringi """ p2 = split_sentences(prev2_txt) p1 = split_sentences(prev1_txt) c = split_sentences(curr_txt) n = split_sentences(next_txt) # case curr >= 4: wszystkie zdania z curr jako embeddingi if len(c) >= 4: return c if len(c) == 3: prev2 = p1[-1] if p1 else "" prev1 = c[0] curr = c[1] #next_s = n[0] if n else "" next_s = c[2] return [prev2, prev1, curr, next_s] if len(c) == 2: prev1_from_curr0 = c[0] curr_sent = c[1] # prev2: ostatnie zdanie z prev1, fallback do ostatniego z prev2 if len(p1) >= 1: prev2_from_p1 = p1[-1] elif len(p2) >= 1: prev2_from_p1 = p2[-1] else: prev2_from_p1 = "" next_s = n[-1] if n else "" return [prev2_from_p1, prev1_from_curr0, curr_sent, next_s] if len(c) == 1: curr_sent = c[0] # prev1 / prev2 zależnie od długości prev1 if len(p1) >= 2: prev1_s = p1[-1] prev2_s = p1[-2] elif len(p1) == 1: prev1_s = p1[0] prev2_s = p2[-1] if p2 else "" else: # brak prev1, spróbuj wypełnić z prev2 if len(p2) >= 2: prev1_s = p2[-1] prev2_s = p2[-2] elif len(p2) == 1: prev1_s = p2[-1] prev2_s = "" else: prev1_s = "" prev2_s = "" next_s = n[0] if n else "" return [prev2_s, prev1_s, curr_sent, next_s] # fallback (curr == 0): we bierzemy ostatnie 4 zdania z prev2+prev1+next all_sents = p2 + p1 + n if not all_sents: return ["", "", "", ""] res = all_sents[-4:] # jeśli mniej niż 4 - dopadamy pustymi stringami z lewej while len(res) < 4: res.insert(0, "") return res def build_s2s_attn_from_predictor_no_max( d_preds: List[torch.Tensor], input_lengths: torch.Tensor, l_min: int = 1, l_max: Optional[int] = 8000, window_radius_tokens: int = 2, # focus: 1–3 is typical token_temp: float = 0.6, # focus: <1 sharper, >1 softer kappa: float = 0.8, # how much to use predictor modulation add_uniform_eps: float = 1e-4, # avoids dead columns ) -> Tuple[torch.Tensor, List[int]]: """ Builds coarse-but-focused attention: - frame-to-token path j_hat from cumulative durations (coarse across frames) - restrict tokens to a small window around j_hat (focused across tokens) - sharpen inside the window with a temperature - optionally modulate by predictor signal resampled to frame grid Returns: s2s_attn: [B, T_max, L_batch_max], column-stochastic output_lengths: list[int] """ device = input_lengths.device B = int(input_lengths.numel()) T_max = int(input_lengths.max().item()) output_lengths: List[int] = [] cache = [] # Pass 1: durations + predicted output length for pred, t_len in zip(d_preds, input_lengths): T = int(t_len.item()) logits = pred[:T].to(device, dtype=torch.float32) # [T, L_pred] probs = torch.sigmoid(logits) # [T, L_pred] dur = probs.sum(dim=-1) + 1e-6 # [T] l_float = dur.sum() if l_max is None: l = int(torch.clamp(l_float.round(), min=l_min).item()) else: l = int(torch.clamp(l_float.round(), min=l_min, max=l_max).item()) # Scale so sum(dur) == l (approximately exact) scale = float(l) / (l_float.item() + 1e-8) dur = dur * scale output_lengths.append(l) cache.append((logits, dur, T, l)) L_batch_max = max(output_lengths) if output_lengths else l_min s2s = torch.zeros(B, T_max, L_batch_max, device=device, dtype=torch.float32) # Pass 2: build windowed attention around path j_hat for b, (logits, dur, T, l) in enumerate(cache): # frame midpoints and cumulative duration to get token index per frame cdf = torch.cumsum(dur, dim=0) # [T] frames = torch.arange(l, device=device, dtype=cdf.dtype) + 0.5 # [l] j_hat = torch.searchsorted(cdf, frames) # [l], long j_hat = j_hat.clamp(min=0, max=T - 1) # Token indices grid t_idx = torch.arange(T, device=device).unsqueeze(1).to(cdf.dtype) # [T,1] j_hat_f = j_hat.unsqueeze(0).to(cdf.dtype) # [1,l] # Window mask around current token index R = int(window_radius_tokens) low = (j_hat - R).clamp(min=0) high = (j_hat + R).clamp(max=T - 1) # allowed[t,f] = low[f] <= t <= high[f] allowed = (t_idx >= low.unsqueeze(0)) & (t_idx <= high.unsqueeze(0)) # [T,l] # Base logits: distance in token index from j_hat (focus within window) # Use a small Gaussian over token index; sigma ~ R / 1.5 sigma_tokens = max(1.0, R / 1.5) attn_logits = -0.5 * ((t_idx - j_hat_f) / sigma_tokens) ** 2 # [T,l] # Optional modulation by predictor (resampled to l frames) if kappa > 0.0: pred_res = F.interpolate( logits.unsqueeze(0), size=l, mode="linear", align_corners=True ).squeeze(0) # [T,l] attn_logits = attn_logits + kappa * torch.log( torch.sigmoid(pred_res) + 1e-6 ) # Mask outside the window attn_logits = attn_logits.masked_fill(~allowed, -1e4) # Sharpen within window with temperature attn = F.softmax(attn_logits / max(1e-4, token_temp), dim=0) # [T,l] # Tiny uniform prior to avoid degenerate columns if add_uniform_eps > 0: attn = attn + add_uniform_eps attn = attn / (attn.sum(dim=0, keepdim=True) + 1e-8) s2s[b, :T, :l] = attn.to(s2s.dtype) return s2s, output_lengths