Download modeling_iceAudio.py from darkps/ice-012-audio: direct link, hf CLI and curl.
- Browser
- Download file 92 kB
-
https://huggingface.co/darkps/ice-012-audio/resolve/main/modeling_iceAudio.py
- Command line
-
hf download hf://darkps/ice-012-audio/modeling_iceAudio.py
-
curl -L -o modeling_iceAudio.py https://huggingface.co/darkps/ice-012-audio/resolve/main/modeling_iceAudio.py
92 kB
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| class RMSNorm(nn.Module): | |
| def __init__(self, d, eps=1e-6): | |
| super().__init__(); self.weight=nn.Parameter(torch.ones(d)); self.eps=eps | |
| def forward(self,x): | |
| return x * torch.rsqrt(x.float().pow(2).mean(-1,keepdim=True)+self.eps).to(x.dtype) * self.weight | |
| class HybridPosition(nn.Module): | |
| def __init__(self, head_dim, theta=10000.0): | |
| super().__init__(); self.head_dim=head_dim; self.theta=theta | |
| inv=1.0/(theta**(torch.arange(0,head_dim,2).float()/head_dim)); self.register_buffer('inv_freq',inv) | |
| self.relative_scale=nn.Parameter(torch.tensor(0.1)) | |
| def forward(self,q,k): | |
| s=q.size(-2); pos=torch.arange(s,device=q.device).float(); freqs=torch.einsum('s,d->sd',pos,self.inv_freq.to(q.device)); emb=torch.cat([freqs,freqs],-1); c,sn=emb.cos(),emb.sin() | |
| def rot(x): return x*c[None,None,:,:]+torch.cat([-x[...,x.size(-1)//2:],x[...,:x.size(-1)//2]],-1)*sn[None,None,:,:] | |
| return rot(q),rot(k) | |
| class MixedAttention(nn.Module): | |
| def __init__(self,d,heads,kv_heads,layer_idx,local_window=256,theta=2500000.0): | |
| super().__init__() | |
| self.d=d; self.h=heads; self.kv=kv_heads; self.hd=128; self.groups=heads//kv_heads; self.local_window=local_window | |
| self.q=nn.Linear(d,heads*self.hd,bias=False); self.k=nn.Linear(d,kv_heads*self.hd,bias=False); self.v=nn.Linear(d,kv_heads*self.hd,bias=False); self.o=nn.Linear(heads*self.hd,d,bias=False) | |
| self.qn=RMSNorm(self.hd); self.kn=RMSNorm(self.hd); self.pos=HybridPosition(self.hd, theta=float(theta)) | |
| self.local_gate=nn.Parameter(torch.tensor(0.5)); self.global_gate=nn.Parameter(torch.tensor(0.5)); self.layer_idx=layer_idx | |
| def forward(self,x,mask=None): | |
| b,s,_=x.shape; q=self.q(x).view(b,s,self.h,self.hd).transpose(1,2); k=self.k(x).view(b,s,self.kv,self.hd).transpose(1,2); v=self.v(x).view(b,s,self.kv,self.hd).transpose(1,2) | |
| q=self.qn(q); k=self.kn(k); q,k=self.pos(q,k); k=k.repeat_interleave(self.groups,1); v=v.repeat_interleave(self.groups,1) | |
| score=torch.matmul(q.float(),k.float().transpose(-1,-2))/math.sqrt(self.hd) | |
| idx=torch.arange(s,device=x.device); causal=idx[None,:]>idx[:,None]; dist=(idx[None,:]-idx[:,None]).abs(); local=dist>self.local_window | |
| score=score.masked_fill(causal[None,None],-torch.inf) | |
| score=score + 0.0*torch.where(local[None,None],self.global_gate.tanh(),self.local_gate.tanh()) | |
| if mask is not None: | |
| if mask.ndim==2: score=score.masked_fill(~mask[:,None,None,:],-torch.inf) | |
| elif mask.ndim==4: score=score.masked_fill(~mask,-torch.inf) if mask.dtype==torch.bool else score+mask | |
| p=F.softmax(score,dim=-1); y=torch.matmul(p,v.float()).to(x.dtype).transpose(1,2).reshape(b,s,self.h*self.hd); return self.o(y) | |
| class SwiGLU(nn.Module): | |
| def __init__(self,d,hidden): | |
| super().__init__(); self.g=nn.Linear(d,hidden,bias=False); self.u=nn.Linear(d,hidden,bias=False); self.o=nn.Linear(hidden,d,bias=False); self.scale=nn.Parameter(torch.tensor(1.0)) | |
| def forward(self,x): return self.o(F.silu(self.g(x))*self.u(x))*self.scale | |
| class IndependentBlock(nn.Module): | |
| def __init__(self,config,layer_idx): | |
| super().__init__(); d=config.hidden_size; self.n1=RMSNorm(d); self.attn=MixedAttention(d,config.num_attention_heads,config.num_key_value_heads,layer_idx,theta=float(getattr(config,'rope_theta',2500000.0))); self.n2=RMSNorm(d); self.ff=SwiGLU(d,config.intermediate_size); self.attn_gate=nn.Parameter(torch.zeros(1)); self.ff_gate=nn.Parameter(torch.zeros(1)) | |
| def forward(self,x,mask=None): | |
| x=x+self.attn_gate.tanh()*self.attn(self.n1(x),mask); return x+self.ff_gate.tanh()*self.ff(self.n2(x)) | |
| class IndependentBackbone(nn.Module): | |
| def __init__(self,config): | |
| super().__init__(); self.config=config; d=config.hidden_size; self.embed=nn.Embedding(config.vocab_size,d); self.blocks=nn.ModuleList([IndependentBlock(config,i) for i in range(config.num_hidden_layers)]); self.norm=RMSNorm(d) | |
| def get_input_embeddings(self): return self.embed | |
| def forward(self,input_ids=None,inputs_embeds=None,attention_mask=None,position_ids=None,return_dict=True,**kwargs): | |
| x=self.embed(input_ids) if inputs_embeds is None else inputs_embeds | |
| for block in self.blocks: x=block(x,attention_mask) | |
| x=self.norm(x); return type('BackboneOutput',(),{'__getitem__':lambda self,i:x,'last_hidden_state':x})() | |
| import torch | |
| import torch.nn as nn | |
| class AcousticProsodyAdapter(nn.Module): | |
| def __init__(self, hidden_size=1024, ff_size=4096, num_layers=7, dropout=0.0): | |
| super().__init__() | |
| self.hidden_size = hidden_size | |
| self.audio_norm = nn.LayerNorm(hidden_size) | |
| self.speaker_encoder = nn.Sequential( | |
| nn.Linear(hidden_size, 2048), nn.SiLU(), nn.Linear(2048, hidden_size) | |
| ) | |
| self.prosody_encoder = nn.Sequential( | |
| nn.Linear(hidden_size, 4096), nn.SiLU(), nn.Linear(4096, hidden_size) | |
| ) | |
| layer = nn.TransformerEncoderLayer( | |
| d_model=hidden_size, | |
| nhead=16, | |
| dim_feedforward=ff_size, | |
| dropout=dropout, | |
| activation="gelu", | |
| batch_first=True, | |
| norm_first=True, | |
| ) | |
| self.temporal_stack = nn.TransformerEncoder(layer, num_layers=num_layers) | |
| self.output_norm = nn.LayerNorm(hidden_size) | |
| self.output_projection = nn.Linear(hidden_size, hidden_size) | |
| self.residual_gate = nn.Parameter(torch.tensor([0.01])) | |
| def _mask_2d(audio_mask): | |
| mask = audio_mask.to(dtype=torch.bool) | |
| if mask.ndim == 3: | |
| mask = mask[:, 0] | |
| return mask | |
| def _route(self, hidden_states, audio_mask): | |
| mask = self._mask_2d(audio_mask) | |
| x = self.audio_norm(hidden_states) | |
| pooled = (x * mask.unsqueeze(-1)).sum(dim=1) | |
| denom = mask.sum(dim=1, keepdim=True).clamp_min(1).to(x.dtype) | |
| pooled = pooled / denom | |
| speaker = self.speaker_encoder(pooled).unsqueeze(1) | |
| prosody = self.prosody_encoder(pooled).unsqueeze(1) | |
| y = self.temporal_stack(x + speaker + prosody) | |
| y = self.output_projection(self.output_norm(y)) | |
| return y | |
| def condition_audio_embeddings(self, audio_embeddings, audio_mask): | |
| return audio_embeddings + self.residual_gate.tanh() * self._route( | |
| audio_embeddings, audio_mask | |
| ) | |
| def refine_hidden_states(self, hidden_states, audio_mask): | |
| return hidden_states + self.residual_gate.tanh() * self._route( | |
| hidden_states, audio_mask | |
| ) | |
| def forward(self, hidden_states, audio_mask): | |
| return self.refine_hidden_states(hidden_states, audio_mask) | |
| def parameter_count(self): | |
| return sum(p.numel() for p in self.parameters()) | |
| import difflib | |
| import logging | |
| import math | |
| import os | |
| import re | |
| from dataclasses import dataclass, fields | |
| from functools import partial | |
| from typing import Any, List, Optional, Union | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from transformers.models.qwen3.configuration_qwen3 import Qwen3Config | |
| from transformers.models.qwen3.modeling_qwen3 import Qwen3Model | |
| import torchaudio | |
| try: | |
| from torch.nn.attention.flex_attention import create_block_mask | |
| _flex_attention_available = True | |
| except ImportError: | |
| _flex_attention_available = False | |
| from transformers import ( | |
| AutoFeatureExtractor, | |
| AutoModel, | |
| AutoTokenizer, | |
| HiggsAudioV2TokenizerModel, | |
| PretrainedConfig, | |
| PreTrainedModel, | |
| ) | |
| from transformers.modeling_outputs import ModelOutput, BaseModelOutputWithPast | |
| from transformers.models.auto import CONFIG_MAPPING, AutoConfig, AutoModelForCausalLM | |
| from .configuration_iceAudio import iceAudioConfig, iceAudioLanguageConfig, iceReengineeredAudioConfig | |
| import io | |
| import logging | |
| import numpy as np | |
| import soundfile as sf | |
| import torch | |
| import torchaudio | |
| from pydub import AudioSegment | |
| from pydub.silence import detect_leading_silence, detect_nonsilent, split_on_silence | |
| logger = logging.getLogger(__name__) | |
| def load_waveform(audio_path: str): | |
| try: | |
| data, sr = sf.read(audio_path, dtype="float32", always_2d=True) | |
| return data.T, sr | |
| except Exception: | |
| import librosa | |
| data, sr = librosa.load(audio_path, sr=None, mono=False) | |
| if data.ndim == 1: | |
| data = data[np.newaxis, :] | |
| return data, sr | |
| def load_audio(audio_path: str, sampling_rate: int) -> np.ndarray: | |
| data, sr = load_waveform(audio_path) | |
| if data.shape[0] > 1: | |
| data = np.mean(data, axis=0, keepdims=True) | |
| if sr != sampling_rate: | |
| data = torchaudio.functional.resample( | |
| torch.from_numpy(data), orig_freq=sr, new_freq=sampling_rate | |
| ).numpy() | |
| return data | |
| def load_audio_bytes(raw: bytes, sampling_rate: int) -> np.ndarray: | |
| buf = io.BytesIO(raw) | |
| try: | |
| data, sr = sf.read(buf, dtype="float32", always_2d=True) | |
| data = data.T | |
| except Exception: | |
| import librosa | |
| buf.seek(0) | |
| data, sr = librosa.load(buf, sr=None, mono=False) | |
| if data.ndim == 1: | |
| data = data[np.newaxis, :] | |
| if data.shape[0] > 1: | |
| data = np.mean(data, axis=0, keepdims=True) | |
| if sr != sampling_rate: | |
| data = torchaudio.functional.resample( | |
| torch.from_numpy(data), orig_freq=sr, new_freq=sampling_rate | |
| ).numpy() | |
| return data | |
| def numpy_to_audiosegment(audio: np.ndarray, sample_rate: int) -> AudioSegment: | |
| audio_int = (audio * 32768.0).clip(-32768, 32767).astype(np.int16) | |
| if audio_int.shape[0] > 1: | |
| audio_int = audio_int.T.flatten() | |
| return AudioSegment( | |
| data=audio_int.tobytes(), | |
| sample_width=2, | |
| frame_rate=sample_rate, | |
| channels=audio.shape[0], | |
| ) | |
| def audiosegment_to_numpy(aseg: AudioSegment) -> np.ndarray: | |
| data = np.array(aseg.get_array_of_samples()).astype(np.float32) / 32768.0 | |
| if aseg.channels == 1: | |
| return data[np.newaxis, :] | |
| return data.reshape(-1, aseg.channels).T | |
| def remove_silence( | |
| audio: np.ndarray, | |
| sampling_rate: int, | |
| mid_sil: int = 300, | |
| lead_sil: int = 100, | |
| trail_sil: int = 300, | |
| ) -> np.ndarray: | |
| wave = numpy_to_audiosegment(audio, sampling_rate) | |
| if mid_sil > 0: | |
| non_silent_segs = split_on_silence( | |
| wave, | |
| min_silence_len=mid_sil, | |
| silence_thresh=-50, | |
| keep_silence=mid_sil, | |
| seek_step=10, | |
| ) | |
| wave = AudioSegment.silent(duration=0) | |
| for seg in non_silent_segs: | |
| wave += seg | |
| wave = remove_silence_edges(wave, lead_sil, trail_sil, -50) | |
| return audiosegment_to_numpy(wave) | |
| def remove_silence_edges( | |
| audio: AudioSegment, | |
| lead_sil: int = 100, | |
| trail_sil: int = 300, | |
| silence_threshold: float = -50, | |
| ) -> AudioSegment: | |
| start_idx = detect_leading_silence(audio, silence_threshold=silence_threshold) | |
| start_idx = max(0, start_idx - lead_sil) | |
| audio = audio[start_idx:] | |
| audio = audio.reverse() | |
| start_idx = detect_leading_silence(audio, silence_threshold=silence_threshold) | |
| start_idx = max(0, start_idx - trail_sil) | |
| audio = audio[start_idx:] | |
| audio = audio.reverse() | |
| return audio | |
| def fade_and_pad_audio( | |
| audio: np.ndarray, | |
| pad_duration: float = 0.1, | |
| fade_duration: float = 0.1, | |
| sample_rate: int = 24000, | |
| ) -> np.ndarray: | |
| if audio.shape[-1] == 0: | |
| return audio | |
| fade_samples = int(fade_duration * sample_rate) | |
| pad_samples = int(pad_duration * sample_rate) | |
| processed = audio.copy() | |
| if fade_samples > 0: | |
| k = min(fade_samples, processed.shape[-1] // 2) | |
| if k > 0: | |
| fade_in = np.linspace(0, 1, k, dtype=np.float32)[np.newaxis, :] | |
| processed[..., :k] *= fade_in | |
| fade_out = np.linspace(1, 0, k, dtype=np.float32)[np.newaxis, :] | |
| processed[..., -k:] *= fade_out | |
| if pad_samples > 0: | |
| silence = np.zeros( | |
| (processed.shape[0], pad_samples), | |
| dtype=processed.dtype, | |
| ) | |
| processed = np.concatenate([silence, processed, silence], axis=-1) | |
| return processed | |
| def trim_long_audio( | |
| audio: np.ndarray, | |
| sampling_rate: int, | |
| max_duration: float = 15.0, | |
| min_duration: float = 3.0, | |
| trim_threshold: float = 20.0, | |
| ) -> np.ndarray: | |
| duration = audio.shape[-1] / sampling_rate | |
| if duration <= trim_threshold: | |
| return audio | |
| seg = numpy_to_audiosegment(audio, sampling_rate) | |
| nonsilent = detect_nonsilent( | |
| seg, min_silence_len=100, silence_thresh=-40, seek_step=10 | |
| ) | |
| if not nonsilent: | |
| return audio | |
| max_ms = int(max_duration * 1000) | |
| min_ms = int(min_duration * 1000) | |
| best_split = 0 | |
| for start, end in nonsilent: | |
| if start > best_split and start <= max_ms: | |
| best_split = start | |
| if end > max_ms: | |
| break | |
| if best_split < min_ms: | |
| best_split = min(max_ms, len(seg)) | |
| trimmed = seg[:best_split] | |
| return audiosegment_to_numpy(trimmed) | |
| def cross_fade_chunks( | |
| chunks: list[np.ndarray], | |
| sample_rate: int, | |
| silence_duration: float = 0.3, | |
| ) -> np.ndarray: | |
| if len(chunks) == 1: | |
| return chunks[0] | |
| total_n = int(silence_duration * sample_rate) | |
| fade_n = total_n // 3 | |
| silence_n = fade_n | |
| merged = chunks[0].copy() | |
| for chunk in chunks[1:]: | |
| parts = [merged] | |
| fout_n = min(fade_n, merged.shape[-1]) | |
| if fout_n > 0: | |
| w_out = np.linspace(1, 0, fout_n, dtype=np.float32)[np.newaxis, :] | |
| parts[-1][..., -fout_n:] *= w_out | |
| parts.append(np.zeros((chunks[0].shape[0], silence_n), dtype=np.float32)) | |
| fade_in = chunk.copy() | |
| fin_n = min(fade_n, fade_in.shape[-1]) | |
| if fin_n > 0: | |
| w_in = np.linspace(0, 1, fin_n, dtype=np.float32)[np.newaxis, :] | |
| fade_in[..., :fin_n] *= w_in | |
| parts.append(fade_in) | |
| merged = np.concatenate(parts, axis=-1) | |
| return merged | |
| import bisect | |
| import unicodedata | |
| from functools import lru_cache | |
| from typing import Optional | |
| class RuleDurationEstimator: | |
| def __init__(self): | |
| self.weights = { | |
| "cjk": 3.0, | |
| "hangul": 2.5, | |
| "kana": 2.2, | |
| "ethiopic": 3.0, | |
| "yi": 3.0, | |
| "indic": 1.8, | |
| "thai_lao": 1.5, | |
| "khmer_myanmar": 1.8, | |
| "arabic": 1.5, | |
| "hebrew": 1.5, | |
| "latin": 1.0, | |
| "cyrillic": 1.0, | |
| "greek": 1.0, | |
| "armenian": 1.0, | |
| "georgian": 1.0, | |
| "punctuation": 0.5, | |
| "space": 0.2, | |
| "digit": 3.5, | |
| "mark": 0.0, | |
| "default": 1.0, | |
| } | |
| self.ranges = [ | |
| (0x02AF, "latin"), | |
| (0x03FF, "greek"), | |
| (0x052F, "cyrillic"), | |
| (0x058F, "armenian"), | |
| (0x05FF, "hebrew"), | |
| (0x077F, "arabic"), | |
| (0x089F, "arabic"), | |
| (0x08FF, "arabic"), | |
| (0x097F, "indic"), | |
| (0x09FF, "indic"), | |
| (0x0A7F, "indic"), | |
| (0x0AFF, "indic"), | |
| (0x0B7F, "indic"), | |
| (0x0BFF, "indic"), | |
| (0x0C7F, "indic"), | |
| (0x0CFF, "indic"), | |
| (0x0D7F, "indic"), | |
| (0x0DFF, "indic"), | |
| (0x0EFF, "thai_lao"), | |
| (0x0FFF, "indic"), | |
| (0x109F, "khmer_myanmar"), | |
| (0x10FF, "georgian"), | |
| (0x11FF, "hangul"), | |
| (0x137F, "ethiopic"), | |
| (0x139F, "ethiopic"), | |
| (0x13FF, "default"), | |
| (0x167F, "default"), | |
| (0x169F, "default"), | |
| (0x16FF, "default"), | |
| (0x171F, "default"), | |
| (0x173F, "default"), | |
| (0x175F, "default"), | |
| (0x177F, "default"), | |
| (0x17FF, "khmer_myanmar"), | |
| (0x18AF, "default"), | |
| (0x18FF, "default"), | |
| (0x194F, "indic"), | |
| (0x19DF, "indic"), | |
| (0x19FF, "khmer_myanmar"), | |
| (0x1A1F, "indic"), | |
| (0x1AAF, "indic"), | |
| (0x1B7F, "indic"), | |
| (0x1BBF, "indic"), | |
| (0x1BFF, "indic"), | |
| (0x1C4F, "indic"), | |
| (0x1C7F, "indic"), | |
| (0x1C8F, "cyrillic"), | |
| (0x1CBF, "georgian"), | |
| (0x1CCF, "indic"), | |
| (0x1CFF, "indic"), | |
| (0x1D7F, "latin"), | |
| (0x1DBF, "latin"), | |
| (0x1DFF, "default"), | |
| (0x1EFF, "latin"), | |
| (0x309F, "kana"), | |
| (0x30FF, "kana"), | |
| (0x312F, "cjk"), | |
| (0x318F, "hangul"), | |
| (0x9FFF, "cjk"), | |
| (0xA4CF, "yi"), | |
| (0xA4FF, "default"), | |
| (0xA63F, "default"), | |
| (0xA69F, "cyrillic"), | |
| (0xA6FF, "default"), | |
| (0xA7FF, "latin"), | |
| (0xA82F, "indic"), | |
| (0xA87F, "default"), | |
| (0xA8DF, "indic"), | |
| (0xA8FF, "indic"), | |
| (0xA92F, "indic"), | |
| (0xA95F, "indic"), | |
| (0xA97F, "hangul"), | |
| (0xA9DF, "indic"), | |
| (0xA9FF, "khmer_myanmar"), | |
| (0xAA5F, "indic"), | |
| (0xAA7F, "khmer_myanmar"), | |
| (0xAADF, "indic"), | |
| (0xAAFF, "indic"), | |
| (0xAB2F, "ethiopic"), | |
| (0xAB6F, "latin"), | |
| (0xABBF, "default"), | |
| (0xABFF, "indic"), | |
| (0xD7AF, "hangul"), | |
| (0xFAFF, "cjk"), | |
| (0xFDFF, "arabic"), | |
| (0xFE6F, "default"), | |
| (0xFEFF, "arabic"), | |
| (0xFFEF, "latin"), | |
| ] | |
| self.breakpoints = [r[0] for r in self.ranges] | |
| def _get_char_weight(self, char): | |
| code = ord(char) | |
| if (65 <= code <= 90) or (97 <= code <= 122): | |
| return self.weights["latin"] | |
| if code == 32: | |
| return self.weights["space"] | |
| if code == 0x0640: | |
| return self.weights["mark"] | |
| category = unicodedata.category(char) | |
| if category.startswith("M"): | |
| return self.weights["mark"] | |
| if category.startswith("P") or category.startswith("S"): | |
| return self.weights["punctuation"] | |
| if category.startswith("Z"): | |
| return self.weights["space"] | |
| if category.startswith("N"): | |
| return self.weights["digit"] | |
| idx = bisect.bisect_left(self.breakpoints, code) | |
| if idx < len(self.ranges): | |
| script_type = self.ranges[idx][1] | |
| return self.weights.get(script_type, self.weights["default"]) | |
| if code > 0x20000: | |
| return self.weights["cjk"] | |
| return self.weights["default"] | |
| def calculate_total_weight(self, text): | |
| return sum(self._get_char_weight(c) for c in text) | |
| def estimate_duration( | |
| self, | |
| target_text: str, | |
| ref_text: str, | |
| ref_duration: float, | |
| low_threshold: Optional[float] = 50, | |
| boost_strength: float = 3, | |
| ) -> float: | |
| if ref_duration <= 0 or not ref_text: | |
| return 0.0 | |
| ref_weight = self.calculate_total_weight(ref_text) | |
| if ref_weight == 0: | |
| return 0.0 | |
| speed_factor = ref_weight / ref_duration | |
| target_weight = self.calculate_total_weight(target_text) | |
| estimated_duration = target_weight / speed_factor | |
| if low_threshold is not None and estimated_duration < low_threshold: | |
| alpha = 1.0 / boost_strength | |
| return low_threshold * (estimated_duration / low_threshold) ** alpha | |
| else: | |
| return estimated_duration | |
| LANG_NAME_TO_ID = {'egyptian lahgtna': 'eg', | |
| 'saudi lahgtna': 'sa', | |
| 'moroccan lahgtna': 'ma', | |
| 'bahraini lahgtna': 'bh', | |
| 'sudanese lahgtna': 'sd', | |
| 'iraqi lahgtna': 'iq', | |
| 'lebanese lahgtna': 'lb', | |
| 'syrian lahgtna': 'sy', | |
| 'libyan lahgtna': 'ly', | |
| 'palestinian lahgtna': 'ps', | |
| 'tunisian lahgtna': 'tn', | |
| 'algerian lahgtna': 'dz', | |
| 'yemeni lahgtna': 'ye', | |
| 'abkhazian': 'ab', | |
| 'afrikaans': 'af', | |
| 'albanian': 'sq', | |
| 'algerian arabic': 'arq', | |
| 'algerian saharan arabic': 'aao', | |
| 'amharic': 'am', | |
| 'armenian': 'hy', | |
| 'assamese': 'as', | |
| 'asturian': 'ast', | |
| 'azerbaijani': 'az', | |
| 'baharna arabic': 'abv', | |
| 'banjar': 'bjn', | |
| 'bashkir': 'ba', | |
| 'basque': 'eu', | |
| 'batak mandailing': 'btm', | |
| 'belarusian': 'be', | |
| 'bengali': 'bn', | |
| 'betawi': 'bew', | |
| 'bhojpuri': 'bho', | |
| 'bosnian': 'bs', | |
| 'breton': 'br', | |
| 'buginese': 'bug', | |
| 'bukharic': 'bhh', | |
| 'bulgarian': 'bg', | |
| 'bulu (cameroon)': 'bum', | |
| 'bundeli': 'bns', | |
| 'burmese': 'my', | |
| 'burushaski': 'bsk', | |
| 'cajatambo north lima quechua': 'qvl', | |
| 'cameroon pidgin': 'wes', | |
| 'campidanese sardinian': 'sro', | |
| 'cantonese': 'yue', | |
| 'catalan': 'ca', | |
| 'cebuano': 'ceb', | |
| 'central kurdish': 'ckb', | |
| 'central nahuatl': 'nhn', | |
| 'central pashto': 'pst', | |
| 'central puebla nahuatl': 'ncx', | |
| 'central yupik': 'esu', | |
| 'central-eastern niger fulfulde': 'fuq', | |
| 'chadian arabic': 'shu', | |
| 'chichewa': 'ny', | |
| 'chichicapan zapotec': 'zpv', | |
| 'chiga': 'cgg', | |
| 'chimalapa zoque': 'zoh', | |
| 'chimborazo highland quichua': 'qug', | |
| 'chinese': 'zh', | |
| 'chiquián ancash quechua': 'qxa', | |
| 'chitwania tharu': 'the', | |
| 'chokwe': 'cjk', | |
| 'chuvash': 'cv', | |
| 'cibak': 'ckl', | |
| 'coastal konjo': 'kjc', | |
| 'copainalá zoque': 'zoc', | |
| 'cornish': 'kw', | |
| 'corongo ancash quechua': 'qwa', | |
| 'croatian': 'hr', | |
| 'cross river mbembe': 'mfn', | |
| 'cuyamecalco mixtec': 'xtu', | |
| 'czech': 'cs', | |
| 'dadiya': 'dbd', | |
| 'dagbani': 'dag', | |
| 'dameli': 'dml', | |
| 'danish': 'da', | |
| 'dargwa': 'dar', | |
| 'dazaga': 'dzg', | |
| 'deccan': 'dcc', | |
| 'degema': 'deg', | |
| 'dera (nigeria)': 'kna', | |
| 'dghwede': 'dgh', | |
| 'dhatki': 'mki', | |
| 'dhivehi': 'dv', | |
| 'dhofari arabic': 'adf', | |
| 'dijim-bwilim': 'cfa', | |
| 'dogri': 'dgo', | |
| 'domaaki': 'dmk', | |
| 'dotyali': 'dty', | |
| 'duala': 'dua', | |
| 'dutch': 'nl', | |
| 'dũya': 'ldb', | |
| 'dyula': 'dyu', | |
| 'eastern balochi': 'bgp', | |
| 'eastern bolivian guaraní': 'gui', | |
| 'eastern egyptian bedawi arabic': 'avl', | |
| 'eastern krahn': 'kqo', | |
| 'eastern mari': 'mhr', | |
| 'eastern yiddish': 'ydd', | |
| 'ebrié': 'ebr', | |
| 'eggon': 'ego', | |
| 'egyptian arabic': 'arz', | |
| 'ejagham': 'etu', | |
| 'eleme': 'elm', | |
| 'eloyi': 'afo', | |
| 'embu': 'ebu', | |
| 'english': 'en', | |
| 'erzya': 'myv', | |
| 'esan': 'ish', | |
| 'esperanto': 'eo', | |
| 'estonian': 'et', | |
| 'eton (cameroon)': 'eto', | |
| 'ewondo': 'ewo', | |
| 'extremaduran': 'ext', | |
| 'fang (equatorial guinea)': 'fan', | |
| 'fanti': 'fat', | |
| 'farefare': 'gur', | |
| "fe'fe'": 'fmp', | |
| 'filipino': 'fil', | |
| 'filomena mata-coahuitlán totonac': 'tlp', | |
| 'finnish': 'fi', | |
| 'fipa': 'fip', | |
| 'french': 'fr', | |
| 'fulah': 'ff', | |
| 'galician': 'gl', | |
| 'gambian wolof': 'wof', | |
| 'ganda': 'lg', | |
| 'garhwali': 'gbm', | |
| 'gawar-bati': 'gwt', | |
| 'gawri': 'gwc', | |
| 'gbagyi': 'gbr', | |
| 'gbari': 'gby', | |
| 'geji': 'gyz', | |
| 'gen': 'gej', | |
| 'georgian': 'ka', | |
| 'german': 'de', | |
| 'geser-gorom': 'ges', | |
| 'gheg albanian': 'aln', | |
| "ghomálá'": 'bbj', | |
| 'gidar': 'gid', | |
| 'glavda': 'glw', | |
| 'goan konkani': 'gom', | |
| 'goaria': 'gig', | |
| 'goemai': 'ank', | |
| 'gola': 'gol', | |
| 'greek': 'el', | |
| 'guarani': 'gn', | |
| 'guduf-gava': 'gdf', | |
| 'guerrero amuzgo': 'amu', | |
| 'gujarati': 'gu', | |
| 'gujari': 'gju', | |
| 'gulf arabic': 'afb', | |
| 'gurgula': 'ggg', | |
| 'gusii': 'guz', | |
| 'gusilay': 'gsl', | |
| 'gweno': 'gwe', | |
| 'güilá zapotec': 'ztu', | |
| 'hadothi': 'hoj', | |
| 'hahon': 'hah', | |
| 'haitian': 'ht', | |
| 'hakha chin': 'cnh', | |
| 'hakö': 'hao', | |
| 'halia': 'hla', | |
| 'hausa': 'ha', | |
| 'hawaiian': 'haw', | |
| 'hazaragi': 'haz', | |
| 'hebrew': 'he', | |
| 'hemba': 'hem', | |
| 'herero': 'hz', | |
| 'highland konjo': 'kjk', | |
| 'hijazi arabic': 'acw', | |
| 'hindi': 'hi', | |
| 'huarijio': 'var', | |
| 'huautla mazatec': 'mau', | |
| 'huaxcaleca nahuatl': 'nhq', | |
| 'huba': 'hbb', | |
| 'huitepec mixtec': 'mxs', | |
| 'hula': 'hul', | |
| 'hungarian': 'hu', | |
| 'hunjara-kaina ke': 'hkk', | |
| 'hwana': 'hwo', | |
| 'ibibio': 'ibb', | |
| 'icelandic': 'is', | |
| 'idakho-isukha-tiriki': 'ida', | |
| 'idoma': 'idu', | |
| 'igbo': 'ig', | |
| 'igo': 'ahl', | |
| 'ikposo': 'kpo', | |
| 'ikwere': 'ikw', | |
| 'imbabura highland quichua': 'qvi', | |
| 'indonesian': 'id', | |
| 'indus kohistani': 'mvy', | |
| 'interlingua (international auxiliary language association)': 'ia', | |
| 'inupiaq': 'ik', | |
| 'irish': 'ga', | |
| 'iron ossetic': 'os', | |
| 'isekiri': 'its', | |
| 'isoko': 'iso', | |
| 'italian': 'it', | |
| 'ito': 'itw', | |
| 'itzá': 'itz', | |
| 'ixtayutla mixtec': 'vmj', | |
| 'izon': 'ijc', | |
| 'jambi malay': 'jax', | |
| 'japanese': 'ja', | |
| 'jaqaru': 'jqr', | |
| 'jauja wanca quechua': 'qxw', | |
| 'jaunsari': 'jns', | |
| 'javanese': 'jv', | |
| 'jiba': 'juo', | |
| 'jju': 'kaj', | |
| 'judeo-moroccan arabic': 'aju', | |
| 'juxtlahuaca mixtec': 'vmc', | |
| 'kabardian': 'kbd', | |
| 'kabras': 'lkb', | |
| 'kabuverdianu': 'kea', | |
| 'kabyle': 'kab', | |
| 'kachi koli': 'gjk', | |
| 'kairak': 'ckr', | |
| 'kalabari': 'ijn', | |
| 'kalasha': 'kls', | |
| 'kalenjin': 'kln', | |
| 'kalkoti': 'xka', | |
| 'kamba': 'kam', | |
| 'kamo': 'kcq', | |
| 'kanauji': 'bjj', | |
| 'kanembu': 'kbl', | |
| 'kannada': 'kn', | |
| 'karekare': 'kai', | |
| 'kashmiri': 'ks', | |
| 'kathoriya tharu': 'tkt', | |
| 'kati': 'bsh', | |
| 'kazakh': 'kk', | |
| 'keiyo': 'eyo', | |
| 'khams tibetan': 'khg', | |
| 'khana': 'ogo', | |
| 'khetrani': 'xhe', | |
| 'khmer': 'km', | |
| 'khowar': 'khw', | |
| 'kinga': 'zga', | |
| 'kinnauri': 'kfk', | |
| 'kinyarwanda': 'rw', | |
| 'kirghiz': 'ky', | |
| 'kirya-konzəl': 'fkk', | |
| 'kochila tharu': 'thq', | |
| 'kohistani shina': 'plk', | |
| 'kohumono': 'bcs', | |
| 'kok borok': 'trp', | |
| 'kol (papua new guinea)': 'kol', | |
| 'kom (cameroon)': 'bkm', | |
| 'koma': 'kmy', | |
| 'konkani': 'knn', | |
| 'konzo': 'koo', | |
| 'korean': 'ko', | |
| 'korwa': 'kfp', | |
| 'kota (india)': 'kfe', | |
| 'koti': 'eko', | |
| 'kuanua': 'ksd', | |
| 'kuanyama': 'kj', | |
| 'kui (india)': 'uki', | |
| 'kulung (nigeria)': 'bbu', | |
| 'kuot': 'kto', | |
| 'kushi': 'kuh', | |
| 'kwambi': 'kwm', | |
| 'kwasio': 'nmg', | |
| 'lala-roba': 'lla', | |
| 'lamang': 'hia', | |
| 'lao': 'lo', | |
| 'larike-wakasihu': 'alo', | |
| 'lasi': 'lss', | |
| 'latgalian': 'ltg', | |
| 'latvian': 'lv', | |
| 'levantine arabic': 'apc', | |
| 'liana-seti': 'ste', | |
| 'liberia kpelle': 'xpe', | |
| 'liberian english': 'lir', | |
| 'libyan arabic': 'ayl', | |
| 'ligurian': 'lij', | |
| 'lijili': 'mgi', | |
| 'lingala': 'ln', | |
| 'lithuanian': 'lt', | |
| 'loarki': 'lrk', | |
| 'logooli': 'rag', | |
| 'logudorese sardinian': 'src', | |
| 'loja highland quichua': 'qvj', | |
| 'loloda': 'loa', | |
| 'longuda': 'lnu', | |
| 'loxicha zapotec': 'ztp', | |
| 'luba-lulua': 'lua', | |
| 'luo': 'luo', | |
| 'lushai': 'lus', | |
| 'luxembourgish': 'lb', | |
| 'maasina fulfulde': 'ffm', | |
| 'maba (chad)': 'mde', | |
| 'macedo-romanian': 'rup', | |
| 'macedonian': 'mk', | |
| 'mada (cameroon)': 'mxu', | |
| 'mafa': 'maf', | |
| 'maithili': 'mai', | |
| 'malay': 'ms', | |
| 'malayalam': 'ml', | |
| 'mali': 'gcc', | |
| "malinaltepec me'phaa": 'tcf', | |
| 'maltese': 'mt', | |
| 'mandara': 'tbf', | |
| 'mandjak': 'mfv', | |
| 'manggarai': 'mqy', | |
| 'manipuri': 'mni', | |
| 'mansoanka': 'msw', | |
| 'manx': 'gv', | |
| 'maori': 'mi', | |
| 'marathi': 'mr', | |
| 'marghi central': 'mrt', | |
| 'marghi south': 'mfm', | |
| 'maria (india)': 'mrr', | |
| 'marwari (pakistan)': 'mve', | |
| 'masana': 'mcn', | |
| 'masikoro malagasy': 'msh', | |
| 'matsés': 'mcf', | |
| 'mazaltepec zapotec': 'zpy', | |
| 'mazatlán mazatec': 'vmz', | |
| 'mazatlán mixe': 'mzl', | |
| 'mbe': 'mfo', | |
| 'mbo (cameroon)': 'mbo', | |
| 'mbum': 'mdd', | |
| 'medumba': 'byv', | |
| 'mekeo': 'mek', | |
| 'meru': 'mer', | |
| 'mesopotamian arabic': 'acm', | |
| 'mewari': 'mtr', | |
| 'min nan chinese': 'nan', | |
| 'mingrelian': 'xmf', | |
| 'mitlatongo mixtec': 'vmm', | |
| 'miya': 'mkf', | |
| 'mokpwe': 'bri', | |
| 'moksha': 'mdf', | |
| 'mom jango': 'ver', | |
| 'mongolian': 'mn', | |
| 'moroccan arabic': 'ary', | |
| 'motu': 'meu', | |
| 'mpiemo': 'mcx', | |
| 'mpumpong': 'mgg', | |
| 'mundang': 'mua', | |
| 'mungaka': 'mhk', | |
| 'musey': 'mse', | |
| 'musgu': 'mug', | |
| 'musi': 'mui', | |
| 'naba': 'mne', | |
| 'najdi arabic': 'ars', | |
| 'nalik': 'nal', | |
| 'nawdm': 'nmz', | |
| 'ndonga': 'ng', | |
| 'neapolitan': 'nap', | |
| 'nepali': 'npi', | |
| 'ngamo': 'nbh', | |
| 'ngas': 'anc', | |
| 'ngiemboon': 'nnh', | |
| 'ngizim': 'ngi', | |
| 'ngomba': 'jgo', | |
| 'ngombale': 'nla', | |
| 'nigerian fulfulde': 'fuv', | |
| 'nigerian pidgin': 'pcm', | |
| 'nimadi': 'noe', | |
| 'nobiin': 'fia', | |
| 'north mesopotamian arabic': 'ayp', | |
| 'north moluccan malay': 'max', | |
| 'northern betsimisaraka malagasy': 'bmm', | |
| 'northern hindko': 'hno', | |
| 'northern kurdish': 'kmr', | |
| 'northern pame': 'pmq', | |
| 'northern pashto': 'pbu', | |
| 'northern uzbek': 'uzn', | |
| 'northwest gbaya': 'gya', | |
| 'norwegian': 'no', | |
| 'norwegian bokmål': 'nb', | |
| 'norwegian nynorsk': 'nn', | |
| 'notsi': 'ncf', | |
| 'nyankpa': 'yes', | |
| 'nyungwe': 'nyu', | |
| 'nzanyi': 'nja', | |
| 'nüpode huitoto': 'hux', | |
| 'occitan': 'oc', | |
| 'od': 'odk', | |
| 'odia': 'ory', | |
| 'odual': 'odu', | |
| 'omani arabic': 'acx', | |
| 'orizaba nahuatl': 'nlv', | |
| 'orma': 'orc', | |
| 'ormuri': 'oru', | |
| 'oromo': 'om', | |
| 'pahari-potwari': 'phr', | |
| 'paiwan': 'pwn', | |
| 'panjabi': 'pa', | |
| 'papuan malay': 'pmy', | |
| 'parkari koli': 'kvx', | |
| 'pedi': 'nso', | |
| 'pero': 'pip', | |
| 'persian': 'fa', | |
| 'petats': 'pex', | |
| 'phalura': 'phl', | |
| 'piemontese': 'pms', | |
| 'piya-kwonci': 'piy', | |
| 'plateau malagasy': 'plt', | |
| 'polish': 'pl', | |
| 'poqomam': 'poc', | |
| 'portuguese': 'pt', | |
| 'pulaar': 'fuc', | |
| 'pular': 'fuf', | |
| 'puno quechua': 'qxp', | |
| 'pushto': 'ps', | |
| 'pökoot': 'pko', | |
| 'qaqet': 'byx', | |
| 'quiotepec chinantec': 'chq', | |
| 'rana tharu': 'thr', | |
| 'rangi': 'lag', | |
| 'rapoisi': 'kyx', | |
| 'ratahan': 'rth', | |
| 'rayón zoque': 'zor', | |
| 'romanian': 'ro', | |
| 'romansh': 'rm', | |
| 'rombo': 'rof', | |
| 'rotokas': 'roo', | |
| 'rukai': 'dru', | |
| 'russian': 'ru', | |
| 'sacapulteco': 'quv', | |
| 'saidi arabic': 'aec', | |
| 'sakalava malagasy': 'skg', | |
| 'sakizaya': 'szy', | |
| 'saleman': 'sau', | |
| 'samba daka': 'ccg', | |
| 'samba leko': 'ndi', | |
| 'san felipe otlaltepec popoloca': 'pow', | |
| 'san francisco del mar huave': 'hue', | |
| 'san juan atzingo popoloca': 'poe', | |
| 'san martín itunyoso triqui': 'trq', | |
| 'san miguel el grande mixtec': 'mig', | |
| 'sansi': 'ssi', | |
| 'sanskrit': 'sa', | |
| 'santa ana de tusi pasco quechua': 'qxt', | |
| 'santa catarina albarradas zapotec': 'ztn', | |
| 'santali': 'sat', | |
| 'santiago del estero quichua': 'qus', | |
| 'saposa': 'sps', | |
| 'saraiki': 'skr', | |
| 'sardinian': 'sc', | |
| 'saya': 'say', | |
| 'sediq': 'trv', | |
| 'serbian': 'sr', | |
| 'seri': 'sei', | |
| 'shina': 'scl', | |
| 'shona': 'sn', | |
| 'siar-lak': 'sjr', | |
| 'sibe': 'nco', | |
| 'sicilian': 'scn', | |
| 'sihuas ancash quechua': 'qws', | |
| 'sikkimese': 'sip', | |
| 'sinaugoro': 'snc', | |
| 'sindhi': 'sd', | |
| 'sindhi bhil': 'sbn', | |
| 'sinhala': 'si', | |
| 'sinicahua mixtec': 'xti', | |
| 'sipacapense': 'qum', | |
| 'siwai': 'siw', | |
| 'slovak': 'sk', | |
| 'slovenian': 'sl', | |
| 'solos': 'sol', | |
| 'somali': 'so', | |
| 'soninke': 'snk', | |
| 'south giziga': 'giz', | |
| 'south ucayali ashéninka': 'cpy', | |
| 'southeastern nochixtlán mixtec': 'mxy', | |
| 'southern betsimisaraka malagasy': 'bzc', | |
| 'southern pashto': 'pbt', | |
| 'southern pastaza quechua': 'qup', | |
| 'soyaltepec mazatec': 'vmp', | |
| 'spanish': 'es', | |
| 'standard arabic': 'arb', | |
| 'standard moroccan tamazight': 'zgh', | |
| 'sudanese arabic': 'apd', | |
| 'sulka': 'sua', | |
| 'svan': 'sva', | |
| 'swahili': 'sw', | |
| 'swedish': 'sv', | |
| "tae'": 'rob', | |
| 'tahaggart tamahaq': 'thv', | |
| 'taita': 'dav', | |
| 'tajik': 'tg', | |
| 'tamil': 'ta', | |
| 'tandroy-mahafaly malagasy': 'tdx', | |
| 'tangale': 'tan', | |
| 'tanosy malagasy': 'txy', | |
| 'tarok': 'yer', | |
| 'tatar': 'tt', | |
| 'tedaga': 'tuq', | |
| 'telugu': 'te', | |
| 'tem': 'kdh', | |
| 'teop': 'tio', | |
| 'tepeuxila cuicatec': 'cux', | |
| 'tepinapa chinantec': 'cte', | |
| 'tera': 'ttr', | |
| 'terei': 'buo', | |
| 'termanu': 'twu', | |
| 'tesaka malagasy': 'tkg', | |
| 'tetelcingo nahuatl': 'nhg', | |
| 'teutila cuicatec': 'cut', | |
| 'thai': 'th', | |
| 'tibetan': 'bo', | |
| 'tidaá mixtec': 'mtx', | |
| 'tidore': 'tvo', | |
| 'tigak': 'tgc', | |
| 'tigre': 'tig', | |
| 'tigrinya': 'ti', | |
| 'tilquiapan zapotec': 'zts', | |
| 'tinputz': 'tpz', | |
| "tlacoapa me'phaa": 'tpl', | |
| 'tlacoatzintepec chinantec': 'ctl', | |
| 'tlingit': 'tli', | |
| 'toki pona': 'tok', | |
| 'tomoip': 'tqp', | |
| 'tondano': 'tdn', | |
| 'tonsea': 'txs', | |
| 'tooro': 'ttj', | |
| 'torau': 'ttu', | |
| 'torwali': 'trw', | |
| 'tsimihety malagasy': 'xmw', | |
| 'tsotso': 'lto', | |
| 'tswana': 'tn', | |
| 'tugen': 'tuy', | |
| 'tuki': 'bag', | |
| 'tula': 'tul', | |
| 'tulu': 'tcy', | |
| 'tunen': 'tvu', | |
| 'tungag': 'lcm', | |
| 'tunisian arabic': 'aeb', | |
| 'tupuri': 'tui', | |
| 'turkana': 'tuv', | |
| 'turkish': 'tr', | |
| 'turkmen': 'tk', | |
| 'tututepec mixtec': 'mtu', | |
| 'twi': 'tw', | |
| 'ubaghara': 'byc', | |
| 'uighur': 'ug', | |
| 'ukrainian': 'uk', | |
| 'umbundu': 'umb', | |
| 'upper sorbian': 'hsb', | |
| 'urdu': 'ur', | |
| 'ushojo': 'ush', | |
| 'uzbek': 'uz', | |
| 'vai': 'vai', | |
| 'vietnamese': 'vi', | |
| 'votic': 'vot', | |
| 'võro': 'vro', | |
| 'waci gbe': 'wci', | |
| 'wadiyara koli': 'kxp', | |
| 'waja': 'wja', | |
| 'wakhi': 'wbl', | |
| 'wanga': 'lwg', | |
| 'wapan': 'juk', | |
| 'warji': 'wji', | |
| 'welsh': 'cy', | |
| 'wemale': 'weo', | |
| 'western frisian': 'fy', | |
| 'western highland purepecha': 'pua', | |
| 'western juxtlahuaca mixtec': 'jmx', | |
| 'western maninkakan': 'mlq', | |
| 'western mari': 'mrj', | |
| 'western niger fulfulde': 'fuh', | |
| 'western panjabi': 'pnb', | |
| 'wolof': 'wo', | |
| 'wuzlam': 'udl', | |
| 'xanaguía zapotec': 'ztg', | |
| 'xhosa': 'xh', | |
| 'yace': 'ekr', | |
| 'yakut': 'sah', | |
| 'yalahatan': 'jal', | |
| 'yanahuanca pasco quechua': 'qur', | |
| 'yangben': 'yav', | |
| 'yaqui': 'yaq', | |
| 'yauyos quechua': 'qux', | |
| 'yekhee': 'ets', | |
| 'yiddish': 'yi', | |
| 'yidgha': 'ydg', | |
| 'yoruba': 'yo', | |
| 'yutanduchi mixtec': 'mab', | |
| 'zacatlán-ahuacatlán-tepetzintla nahuatl': 'nhi', | |
| 'zarma': 'dje', | |
| 'zaza': 'zza', | |
| 'zulu': 'zu', | |
| 'ömie': 'aom'} | |
| LANG_NAMES = set(LANG_NAME_TO_ID.keys()) | |
| LANG_IDS = set(LANG_NAME_TO_ID.values()) | |
| _TITLE_EXCEPTIONS = { | |
| "fe'fe'": "Fe'fe'", | |
| "dũya": "Dũya", | |
| "santiago del estero quichua": "Santiago del Estero Quichua", | |
| "santa ana de tusi pasco quechua": "Santa Ana de Tusi Pasco Quechua", | |
| "malinaltepec me'phaa": "Malinaltepec Me'phaa", | |
| "tlacoapa me'phaa": "Tlacoapa Me'phaa", | |
| } | |
| def lang_display_name(name: str) -> str: | |
| return _TITLE_EXCEPTIONS.get(name, name.title()) | |
| from typing import List, Optional | |
| SPLIT_PUNCTUATION = set(".,;:!?。,;:!?") | |
| CLOSING_MARKS = set("\"'""')]》》>」】") | |
| END_PUNCTUATION = { | |
| ";", | |
| ":", | |
| ",", | |
| ".", | |
| "!", | |
| "?", | |
| "…", | |
| ")", | |
| "]", | |
| "}", | |
| '"', | |
| "'", | |
| """, | |
| "'", | |
| ";", | |
| ":", | |
| ",", | |
| "。", | |
| "!", | |
| "?", | |
| "、", | |
| "……", | |
| ")", | |
| "】", | |
| """, | |
| "'", | |
| } | |
| ABBREVIATIONS = { | |
| "Mr.", | |
| "Mrs.", | |
| "Ms.", | |
| "Dr.", | |
| "Prof.", | |
| "Sr.", | |
| "Jr.", | |
| "Rev.", | |
| "Fr.", | |
| "Hon.", | |
| "Pres.", | |
| "Gov.", | |
| "Capt.", | |
| "Gen.", | |
| "Sen.", | |
| "Rep.", | |
| "Col.", | |
| "Maj.", | |
| "Lt.", | |
| "Cmdr.", | |
| "Sgt.", | |
| "Cpl.", | |
| "Co.", | |
| "Corp.", | |
| "Inc.", | |
| "Ltd.", | |
| "Est.", | |
| "Dept.", | |
| "St.", | |
| "Ave.", | |
| "Blvd.", | |
| "Rd.", | |
| "Mt.", | |
| "Ft.", | |
| "No.", | |
| "Jan.", | |
| "Feb.", | |
| "Mar.", | |
| "Apr.", | |
| "Aug.", | |
| "Sep.", | |
| "Sept.", | |
| "Oct.", | |
| "Nov.", | |
| "Dec.", | |
| "i.e.", | |
| "e.g.", | |
| "vs.", | |
| "Vs.", | |
| "Etc.", | |
| "approx.", | |
| "fig.", | |
| "def.", | |
| } | |
| def chunk_text_punctuation( | |
| text: str, | |
| chunk_len: int, | |
| min_chunk_len: Optional[int] = None, | |
| ) -> List[str]: | |
| sentences = [] | |
| current_sentence = [] | |
| tokens_list = list(text) | |
| for token in tokens_list: | |
| if ( | |
| len(current_sentence) == 0 | |
| and len(sentences) != 0 | |
| and (token in SPLIT_PUNCTUATION or token in CLOSING_MARKS) | |
| ): | |
| sentences[-1].append(token) | |
| else: | |
| current_sentence.append(token) | |
| if token in SPLIT_PUNCTUATION: | |
| is_abbreviation = False | |
| if token == ".": | |
| temp_str = "".join(current_sentence).strip() | |
| if temp_str: | |
| last_word = temp_str.split()[-1] | |
| if last_word in ABBREVIATIONS: | |
| is_abbreviation = True | |
| if not is_abbreviation: | |
| sentences.append(current_sentence) | |
| current_sentence = [] | |
| if len(current_sentence) != 0: | |
| sentences.append(current_sentence) | |
| merged_chunks = [] | |
| current_chunk = [] | |
| for sentence in sentences: | |
| if len(current_chunk) + len(sentence) <= chunk_len: | |
| current_chunk.extend(sentence) | |
| else: | |
| if len(current_chunk) > 0: | |
| merged_chunks.append(current_chunk) | |
| current_chunk = sentence | |
| if len(current_chunk) > 0: | |
| merged_chunks.append(current_chunk) | |
| if min_chunk_len is not None: | |
| first_chunk_short_flag = ( | |
| len(merged_chunks) > 0 and len(merged_chunks[0]) < min_chunk_len | |
| ) | |
| final_chunks = [] | |
| for i, chunk in enumerate(merged_chunks): | |
| if i == 1 and first_chunk_short_flag: | |
| final_chunks[-1].extend(chunk) | |
| else: | |
| if len(chunk) >= min_chunk_len: | |
| final_chunks.append(chunk) | |
| else: | |
| if len(final_chunks) == 0: | |
| final_chunks.append(chunk) | |
| else: | |
| final_chunks[-1].extend(chunk) | |
| else: | |
| final_chunks = merged_chunks | |
| chunk_strings = [ | |
| "".join(chunk).strip() for chunk in final_chunks if "".join(chunk).strip() | |
| ] | |
| return chunk_strings | |
| def add_punctuation(text: str): | |
| text = text.strip() | |
| if not text: | |
| return text | |
| if text[-1] not in END_PUNCTUATION: | |
| is_chinese = any("\u4e00" <= char <= "\u9fff" for char in text) | |
| text += "。" if is_chinese else "." | |
| return text | |
| import re | |
| _ZH_RE = re.compile(r'[\u4e00-\u9fff]') | |
| _INSTRUCT_CATEGORIES = [ | |
| {"male": "男", "female": "女"}, | |
| {"child": "儿童", "teenager": "少年", "young adult": "青年", | |
| "middle-aged": "中年", "elderly": "老年"}, | |
| {"very low pitch": "极低音调", "low pitch": "低音调", | |
| "moderate pitch": "中音调", "high pitch": "高音调", | |
| "very high pitch": "极高音调"}, | |
| {"whisper": "耳语"}, | |
| {"american accent", "british accent", "australian accent", | |
| "chinese accent", "canadian accent", "indian accent", | |
| "korean accent", "portuguese accent", "russian accent", "japanese accent"}, | |
| {"河南话", "陕西话", "四川话", "贵州话", "云南话", "桂林话", | |
| "济南话", "石家庄话", "甘肃话", "宁夏话", "青岛话", "东北话"}, | |
| ] | |
| _INSTRUCT_EN_TO_ZH = {} | |
| _INSTRUCT_ZH_TO_EN = {} | |
| _INSTRUCT_MUTUALLY_EXCLUSIVE = [] | |
| for _cat in _INSTRUCT_CATEGORIES: | |
| if isinstance(_cat, dict): | |
| _INSTRUCT_EN_TO_ZH.update(_cat) | |
| _INSTRUCT_ZH_TO_EN.update({v: k for k, v in _cat.items()}) | |
| _INSTRUCT_MUTUALLY_EXCLUSIVE.append(set(_cat) | set(_cat.values())) | |
| else: | |
| _INSTRUCT_MUTUALLY_EXCLUSIVE.append(set(_cat)) | |
| _INSTRUCT_ALL_VALID = ( | |
| set(_INSTRUCT_EN_TO_ZH) | set(_INSTRUCT_ZH_TO_EN) | |
| | _INSTRUCT_MUTUALLY_EXCLUSIVE[-2] | |
| | _INSTRUCT_MUTUALLY_EXCLUSIVE[-1] | |
| ) | |
| _INSTRUCT_VALID_EN = frozenset(i for i in _INSTRUCT_ALL_VALID if not _ZH_RE.search(i)) | |
| _INSTRUCT_VALID_ZH = frozenset(i for i in _INSTRUCT_ALL_VALID if _ZH_RE.search(i)) | |
| logger = logging.getLogger(__name__) | |
| class VoiceClonePrompt: | |
| ref_audio_tokens: torch.Tensor | |
| ref_text: str | |
| ref_rms: float | |
| class iceAudioGenerationConfig: | |
| num_step: int = 32 | |
| guidance_scale: float = 2.0 | |
| t_shift: float = 0.1 | |
| layer_penalty_factor: float = 5.0 | |
| position_temperature: float = 5.0 | |
| class_temperature: float = 0.0 | |
| denoise: bool = True | |
| preprocess_prompt: bool = True | |
| postprocess_output: bool = True | |
| audio_chunk_duration: float = 15.0 | |
| audio_chunk_threshold: float = 30.0 | |
| def from_dict(cls, kwargs_dict): | |
| valid_keys = {f.name for f in fields(cls)} | |
| filtered = {k: v for k, v in kwargs_dict.items() if k in valid_keys} | |
| return cls(**filtered) | |
| class GenerationTask: | |
| batch_size: int | |
| texts: List[str] | |
| target_lens: List[int] | |
| langs: List[Optional[str]] | |
| instructs: List[Optional[str]] | |
| ref_texts: List[Optional[str]] | |
| ref_audio_tokens: List[Optional[torch.Tensor]] | |
| ref_rms: List[Optional[float]] | |
| speed: Optional[List[float]] = None | |
| def get_indices(self, config: iceAudioGenerationConfig, frame_rate: int): | |
| threshold = int(config.audio_chunk_threshold * frame_rate) | |
| short_idx = [i for i, l in enumerate(self.target_lens) if l <= threshold] | |
| long_idx = [i for i, l in enumerate(self.target_lens) if l > threshold] | |
| return short_idx, long_idx | |
| def slice_task(self, indices: List[int]): | |
| if not indices: | |
| return None | |
| return GenerationTask( | |
| batch_size=len(indices), | |
| texts=[self.texts[i] for i in indices], | |
| target_lens=[self.target_lens[i] for i in indices], | |
| langs=[self.langs[i] for i in indices], | |
| instructs=[self.instructs[i] for i in indices], | |
| ref_texts=[self.ref_texts[i] for i in indices], | |
| ref_audio_tokens=[self.ref_audio_tokens[i] for i in indices], | |
| ref_rms=[self.ref_rms[i] for i in indices], | |
| speed=[self.speed[i] for i in indices] if self.speed else None, | |
| ) | |
| class iceAudioModelOutput(ModelOutput): | |
| loss: Optional[torch.Tensor] = None | |
| logits: Optional[torch.Tensor] = None | |
| def _resolve_model_path(name_or_path: str) -> str: | |
| if os.path.isdir(name_or_path): | |
| return name_or_path | |
| from huggingface_hub import snapshot_download | |
| return snapshot_download(name_or_path) | |
| class IceAudioRMSNorm(nn.Module): | |
| def __init__(self, hidden_size, eps=1e-6): | |
| super().__init__() | |
| self.weight = nn.Parameter(torch.ones(hidden_size)) | |
| self.eps = float(eps) | |
| def forward(self, x): | |
| variance = x.float().pow(2).mean(-1, keepdim=True) | |
| return (x.float() * torch.rsqrt(variance + self.eps) * self.weight).to(x.dtype) | |
| def _rotate_half(x): | |
| half = x.shape[-1] // 2 | |
| return torch.cat((-x[..., half:], x[..., :half]), dim=-1) | |
| class IceAudioRotaryEmbedding(nn.Module): | |
| def __init__(self, dim, base=1000000.0): | |
| super().__init__() | |
| self.dim = int(dim) | |
| self.base = float(base) | |
| inv = 1.0 / (self.base ** (torch.arange(0, self.dim, 2, dtype=torch.float32) / self.dim)) | |
| self.register_buffer('inv_freq', inv, persistent=True) | |
| def forward(self, position_ids, dtype): | |
| inv = self.inv_freq.to(position_ids.device) | |
| freqs = torch.einsum('bs,d->bsd', position_ids.float(), inv) | |
| emb = torch.cat((freqs, freqs), dim=-1) | |
| return emb.cos().to(dtype), emb.sin().to(dtype) | |
| class IceAudioMLP(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| self.gate = nn.Linear(config.hidden_size, config.intermediate_size, bias=False) | |
| self.up = nn.Linear(config.hidden_size, config.intermediate_size, bias=False) | |
| self.down = nn.Linear(config.intermediate_size, config.hidden_size, bias=False) | |
| self.act = getattr(config, 'hidden_act', 'silu') | |
| def forward(self, x): | |
| if self.act == 'silu': | |
| h = F.silu(self.gate(x)) | |
| elif self.act == 'gelu': | |
| h = F.gelu(self.gate(x)) | |
| else: | |
| h = F.silu(self.gate(x)) | |
| return self.down(h * self.up(x)) | |
| class IceAudioAttention(nn.Module): | |
| def __init__(self, config, layer_idx=0): | |
| super().__init__() | |
| self.layer_idx = layer_idx | |
| self.num_heads = int(config.num_attention_heads) | |
| self.num_kv_heads = int(getattr(config, 'num_key_value_heads', self.num_heads)) | |
| self.head_dim = int(getattr(config, 'head_dim', config.hidden_size // self.num_heads)) | |
| self.groups = self.num_heads // self.num_kv_heads | |
| bias = bool(getattr(config, 'attention_bias', False)) | |
| self.query = nn.Linear(config.hidden_size, self.num_heads * self.head_dim, bias=bias) | |
| self.key = nn.Linear(config.hidden_size, self.num_kv_heads * self.head_dim, bias=bias) | |
| self.value = nn.Linear(config.hidden_size, self.num_kv_heads * self.head_dim, bias=bias) | |
| self.q_norm = IceAudioRMSNorm(self.head_dim, getattr(config, 'rms_norm_eps', 1e-6)) | |
| self.k_norm = IceAudioRMSNorm(self.head_dim, getattr(config, 'rms_norm_eps', 1e-6)) | |
| self.output = nn.Linear(self.num_heads * self.head_dim, config.hidden_size, bias=False) | |
| self.rope = IceAudioRotaryEmbedding(self.head_dim, getattr(config, 'rope_theta', 1000000.0)) | |
| def _repeat_kv(self, x): | |
| if self.groups == 1: | |
| return x | |
| b, h, s, d = x.shape | |
| return x[:, :, None, :, :].expand(b, h, self.groups, s, d).reshape(b, h * self.groups, s, d) | |
| def forward(self, hidden_states, attention_mask=None, position_ids=None): | |
| bsz, q_len, _ = hidden_states.shape | |
| q = self.query(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2) | |
| k = self.key(hidden_states).view(bsz, q_len, self.num_kv_heads, self.head_dim).transpose(1, 2) | |
| v = self.value(hidden_states).view(bsz, q_len, self.num_kv_heads, self.head_dim).transpose(1, 2) | |
| q = self.q_norm(q) | |
| k = self.k_norm(k) | |
| if position_ids is None: | |
| position_ids = torch.arange(q_len, device=hidden_states.device).unsqueeze(0).expand(bsz, -1) | |
| cos, sin = self.rope(position_ids, q.dtype) | |
| cos, sin = cos[:, None, :, :], sin[:, None, :, :] | |
| q = q * cos + _rotate_half(q) * sin | |
| k = k * cos + _rotate_half(k) * sin | |
| k = self._repeat_kv(k) | |
| v = self._repeat_kv(v) | |
| scores = torch.matmul(q.float(), k.float().transpose(-2, -1)) / math.sqrt(self.head_dim) | |
| causal = torch.triu(torch.ones(q_len, q_len, device=hidden_states.device, dtype=torch.bool), diagonal=1) | |
| scores = scores.masked_fill(causal[None, None], torch.finfo(scores.dtype).min) | |
| if attention_mask is not None: | |
| mask_value = torch.finfo(scores.dtype).min | |
| if attention_mask.ndim == 2: | |
| if attention_mask.dtype == torch.bool: | |
| scores = scores.masked_fill(~attention_mask[:, None, None, :], mask_value) | |
| else: | |
| scores = scores + (1.0 - attention_mask[:, None, None, :]).float() * mask_value | |
| elif attention_mask.ndim == 4: | |
| if attention_mask.dtype == torch.bool: | |
| scores = scores.masked_fill(~attention_mask, mask_value) | |
| else: | |
| scores = scores + attention_mask.float() | |
| probs = F.softmax(scores, dim=-1, dtype=torch.float32) | |
| out = torch.matmul(probs, v.float()).to(q.dtype) | |
| out = out.transpose(1, 2).contiguous().view(bsz, q_len, self.num_heads * self.head_dim) | |
| return self.output(out) | |
| class IceAudioBlock(nn.Module): | |
| def __init__(self, config, layer_idx=0): | |
| super().__init__() | |
| self.pre_norm = IceAudioRMSNorm(config.hidden_size, getattr(config, 'rms_norm_eps', 1e-6)) | |
| self.attention = IceAudioAttention(config, layer_idx=layer_idx) | |
| self.post_norm = IceAudioRMSNorm(config.hidden_size, getattr(config, 'rms_norm_eps', 1e-6)) | |
| self.mlp = IceAudioMLP(config) | |
| def forward(self, x, attention_mask=None, position_ids=None): | |
| x = x + self.attention(self.pre_norm(x), attention_mask, position_ids) | |
| x = x + self.mlp(self.post_norm(x)) | |
| return x | |
| class IceAudioLanguageModel(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| data = config.to_dict() if hasattr(config, 'to_dict') else dict(config) | |
| data['model_type'] = 'qwen3' | |
| data.pop('architectures', None) | |
| qcfg = Qwen3Config.from_dict(data) | |
| self.config = qcfg | |
| self.model = Qwen3Model(qcfg) | |
| def get_input_embeddings(self): | |
| return self.model.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.model.set_input_embeddings(value) | |
| def get_output_embeddings(self): | |
| return None | |
| def set_output_embeddings(self, value): | |
| raise NotImplementedError('Qwen3Model has no LM head in Ice Audio') | |
| def forward(self, input_ids=None, inputs_embeds=None, attention_mask=None, position_ids=None, return_dict=True, **kwargs): | |
| return self.model(input_ids=input_ids, inputs_embeds=inputs_embeds, | |
| attention_mask=attention_mask, position_ids=position_ids, | |
| return_dict=return_dict, **kwargs) | |
| class iceAudio(PreTrainedModel): | |
| _supports_flex_attn = True | |
| _supports_flash_attn_2 = True | |
| _supports_sdpa = True | |
| config_class = iceAudioConfig | |
| def __init__(self, config: iceAudioConfig, language_model: Optional[PreTrainedModel] = None): | |
| super().__init__(config) | |
| if language_model is not None: | |
| self.language_model = language_model | |
| else: | |
| self.language_model = IceAudioLanguageModel(self.config.language_model_config) | |
| self.audio_embeddings = nn.Embedding( | |
| config.num_audio_codebook * config.audio_vocab_size, | |
| self.config.language_model_config.hidden_size, | |
| ) | |
| self.register_buffer( | |
| "codebook_layer_offsets", | |
| torch.arange(config.num_audio_codebook) * config.audio_vocab_size, | |
| ) | |
| self.audio_heads = nn.Linear( | |
| self.config.language_model_config.hidden_size, | |
| config.num_audio_codebook * config.audio_vocab_size, | |
| bias=False, | |
| ) | |
| self.acoustic_adapter = None | |
| if getattr(config, "acoustic_adapter_enabled", True): | |
| self.acoustic_adapter = AcousticProsodyAdapter( | |
| hidden_size=self.config.language_model_config.hidden_size, | |
| ff_size=getattr(config, "acoustic_adapter_ff_size", 4096), | |
| num_layers=getattr(config, "acoustic_adapter_layers", 7), | |
| ) | |
| self.normalized_audio_codebook_weights = [ | |
| w / sum(config.audio_codebook_weights) | |
| for w in config.audio_codebook_weights | |
| ] | |
| self.post_init() | |
| self.text_tokenizer = None | |
| self.audio_tokenizer = None | |
| self.duration_estimator = None | |
| self.sampling_rate = None | |
| self._asr_pipe = None | |
| def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs): | |
| train_mode = kwargs.pop("train", False) | |
| load_asr = kwargs.pop("load_asr", False) | |
| asr_model_name = kwargs.pop("asr_model_name", "openai/whisper-large-v3-turbo") | |
| _prev_disable = logging.root.manager.disable | |
| logging.disable(logging.INFO) | |
| try: | |
| resolved_path = _resolve_model_path(pretrained_model_name_or_path) | |
| model = super().from_pretrained(resolved_path, *args, **kwargs) | |
| if not train_mode: | |
| model.text_tokenizer = AutoTokenizer.from_pretrained(resolved_path, trust_remote_code=True) | |
| audio_tokenizer_path = os.path.join(resolved_path, "audio_tokenizer") | |
| if not os.path.isdir(audio_tokenizer_path): | |
| audio_tokenizer_path = _resolve_model_path( | |
| "eustlb/higgs-audio-v2-tokenizer" | |
| ) | |
| tokenizer_device = ( | |
| "cpu" if str(model.device).startswith("mps") else model.device | |
| ) | |
| model.audio_tokenizer = HiggsAudioV2TokenizerModel.from_pretrained( | |
| audio_tokenizer_path, device_map=tokenizer_device | |
| ) | |
| model.feature_extractor = AutoFeatureExtractor.from_pretrained( | |
| audio_tokenizer_path | |
| ) | |
| model.sampling_rate = model.feature_extractor.sampling_rate | |
| model.duration_estimator = RuleDurationEstimator() | |
| if load_asr: | |
| model.load_asr_model(model_name=asr_model_name) | |
| finally: | |
| logging.disable(_prev_disable) | |
| return model | |
| def load_asr_model(self, model_name: str = "openai/whisper-large-v3-turbo"): | |
| from transformers import pipeline as hf_pipeline | |
| logger.info("Loading ASR model %s ...", model_name) | |
| asr_dtype = ( | |
| torch.float16 if str(self.device).startswith("cuda") else torch.float32 | |
| ) | |
| model_name = _resolve_model_path(model_name) | |
| self._asr_pipe = hf_pipeline( | |
| "automatic-speech-recognition", | |
| model=model_name, | |
| dtype=asr_dtype, | |
| device_map=self.device, | |
| ) | |
| logger.info("ASR model loaded on %s.", self.device) | |
| def transcribe( | |
| self, | |
| audio: Union[str, tuple], | |
| ) -> str: | |
| if self._asr_pipe is None: | |
| raise RuntimeError( | |
| "ASR model is not loaded. Call model.load_asr_model() first." | |
| ) | |
| if isinstance(audio, str): | |
| return self._asr_pipe(audio)["text"].strip() | |
| else: | |
| waveform, sr = audio | |
| if isinstance(waveform, torch.Tensor): | |
| waveform = waveform.cpu().numpy() | |
| waveform = np.squeeze(waveform) | |
| audio_input = { | |
| "array": waveform, | |
| "sampling_rate": sr, | |
| } | |
| return self._asr_pipe(audio_input)["text"].strip() | |
| def get_input_embeddings(self): | |
| return self.language_model.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.language_model.set_input_embeddings(value) | |
| def _prepare_embed_inputs( | |
| self, input_ids: torch.Tensor, audio_mask: torch.Tensor | |
| ) -> torch.Tensor: | |
| text_embeds = self.get_input_embeddings()(input_ids[:, 0, :]) | |
| shifted_ids = ( | |
| input_ids * audio_mask.unsqueeze(1) | |
| ) + self.codebook_layer_offsets.view(1, -1, 1) | |
| audio_embeds = self.audio_embeddings(shifted_ids).sum(dim=1) | |
| if self.acoustic_adapter is not None: | |
| audio_embeds = self.acoustic_adapter.condition_audio_embeddings( | |
| audio_embeds, audio_mask | |
| ) | |
| return torch.where(audio_mask.unsqueeze(-1), audio_embeds, text_embeds) | |
| def forward( | |
| self, | |
| input_ids: torch.LongTensor, | |
| audio_mask: torch.Tensor, | |
| labels: Optional[torch.LongTensor] = None, | |
| attention_mask: Optional[torch.Tensor] = None, | |
| document_ids: Optional[torch.Tensor] = None, | |
| position_ids: Optional[torch.LongTensor] = None, | |
| ): | |
| inputs_embeds = self._prepare_embed_inputs(input_ids, audio_mask) | |
| if attention_mask is None and document_ids is not None: | |
| if not _flex_attention_available: | |
| raise RuntimeError( | |
| "flex_attention is not available in the current environment. " | |
| "If you do not need flex_attention, set " | |
| '"attn_implementation": "sdpa" in your training config.' | |
| ) | |
| attention_mask = create_block_mask( | |
| _get_packed_mask( | |
| document_ids[0].to(inputs_embeds.device), | |
| ), | |
| B=None, | |
| H=None, | |
| Q_LEN=input_ids.size(-1), | |
| KV_LEN=input_ids.size(-1), | |
| _compile=True, | |
| device=inputs_embeds.device, | |
| ) | |
| llm_outputs = self.language_model( | |
| inputs_embeds=inputs_embeds, | |
| attention_mask=attention_mask, | |
| return_dict=True, | |
| position_ids=position_ids, | |
| ) | |
| hidden_states = llm_outputs[0] | |
| if self.acoustic_adapter is not None: | |
| hidden_states = self.acoustic_adapter.refine_hidden_states( | |
| hidden_states, audio_mask | |
| ) | |
| loss = None | |
| batch_size, seq_len, _ = hidden_states.shape | |
| logits_flat = self.audio_heads(hidden_states) | |
| audio_logits = logits_flat.view( | |
| batch_size, | |
| seq_len, | |
| self.config.num_audio_codebook, | |
| self.config.audio_vocab_size, | |
| ).permute(0, 2, 1, 3) | |
| if labels is not None: | |
| per_token_loss = torch.nn.functional.cross_entropy( | |
| audio_logits.permute(0, 3, 1, 2), | |
| labels, | |
| reduction="none", | |
| ignore_index=-100, | |
| ) | |
| valid_mask = (labels != -100).float() | |
| layer_means = (per_token_loss * valid_mask).sum( | |
| dim=(0, 2) | |
| ) / valid_mask.sum(dim=(0, 2)).clamp(min=1.0) | |
| weights = torch.tensor( | |
| self.normalized_audio_codebook_weights, device=audio_logits.device | |
| ) | |
| loss = (layer_means * weights).sum() | |
| return iceAudioModelOutput( | |
| loss=loss, | |
| logits=audio_logits, | |
| ) | |
| def supported_language_ids(self) -> set[str]: | |
| return LANG_IDS | |
| def supported_language_names(self) -> set[str]: | |
| return LANG_NAMES | |
| def generate( | |
| self, | |
| text: Union[str, list[str]], | |
| language: Union[str, list[str], None] = None, | |
| ref_text: Union[str, list[str], None] = None, | |
| ref_audio: Union[ | |
| str, | |
| list[str], | |
| tuple[torch.Tensor, int], | |
| list[tuple[torch.Tensor, int]], | |
| None, | |
| ] = None, | |
| voice_clone_prompt: Union[ | |
| VoiceClonePrompt, list[VoiceClonePrompt], None | |
| ] = None, | |
| instruct: Union[str, list[str], None] = None, | |
| duration: Union[float, list[Optional[float]], None] = None, | |
| speed: Union[float, list[Optional[float]], None] = None, | |
| generation_config: Optional[iceAudioGenerationConfig] = None, | |
| **kwargs, | |
| ) -> list[np.ndarray]: | |
| if self.audio_tokenizer is None or self.text_tokenizer is None: | |
| raise RuntimeError( | |
| "Model is not loaded with audio/text tokenizers. Make sure you " | |
| "loaded the model with iceAudio.from_pretrained()." | |
| ) | |
| gen_config = ( | |
| generation_config | |
| if generation_config is not None | |
| else iceAudioGenerationConfig.from_dict(kwargs) | |
| ) | |
| self.eval() | |
| full_task = self._preprocess_all( | |
| text=text, | |
| language=language, | |
| ref_text=ref_text, | |
| ref_audio=ref_audio, | |
| voice_clone_prompt=voice_clone_prompt, | |
| instruct=instruct, | |
| preprocess_prompt=gen_config.preprocess_prompt, | |
| speed=speed, | |
| duration=duration, | |
| ) | |
| short_idx, long_idx = full_task.get_indices( | |
| gen_config, self.audio_tokenizer.config.frame_rate | |
| ) | |
| results = [None] * full_task.batch_size | |
| if short_idx: | |
| short_task = full_task.slice_task(short_idx) | |
| short_results = self._generate_iterative(short_task, gen_config) | |
| for idx, res in zip(short_idx, short_results): | |
| results[idx] = res | |
| if long_idx: | |
| long_task = full_task.slice_task(long_idx) | |
| long_results = self._generate_chunked(long_task, gen_config) | |
| for idx, res in zip(long_idx, long_results): | |
| results[idx] = res | |
| generated_audios = [] | |
| for i in range(full_task.batch_size): | |
| assert results[i] is not None, f"Result {i} was not generated" | |
| generated_audios.append( | |
| self._decode_and_post_process( | |
| results[i], full_task.ref_rms[i], gen_config | |
| ) | |
| ) | |
| return generated_audios | |
| def create_voice_clone_prompt( | |
| self, | |
| ref_audio: Union[str, tuple[torch.Tensor, int]], | |
| ref_text: Optional[str] = None, | |
| preprocess_prompt: bool = True, | |
| ) -> VoiceClonePrompt: | |
| if self.audio_tokenizer is None: | |
| raise RuntimeError( | |
| "Audio tokenizer is not loaded. Make sure you loaded the model " | |
| "with iceAudio.from_pretrained()." | |
| ) | |
| if isinstance(ref_audio, str): | |
| ref_wav = load_audio(ref_audio, self.sampling_rate) | |
| else: | |
| waveform, sr = ref_audio | |
| if isinstance(waveform, torch.Tensor): | |
| waveform = waveform.cpu().numpy() | |
| if waveform.ndim == 1: | |
| waveform = waveform[np.newaxis, :] | |
| if waveform.shape[0] > 1: | |
| waveform = np.mean(waveform, axis=0, keepdims=True) | |
| if sr != self.sampling_rate: | |
| waveform = torchaudio.functional.resample( | |
| torch.from_numpy(waveform), | |
| orig_freq=sr, | |
| new_freq=self.sampling_rate, | |
| ).numpy() | |
| ref_wav = waveform | |
| ref_rms = float(np.sqrt(np.mean(ref_wav**2))) | |
| if 0 < ref_rms < 0.1: | |
| ref_wav = ref_wav * 0.1 / ref_rms | |
| if preprocess_prompt: | |
| if ref_text is None: | |
| ref_wav = trim_long_audio( | |
| ref_wav, self.sampling_rate, trim_threshold=20.0 | |
| ) | |
| ref_wav = remove_silence( | |
| ref_wav, | |
| self.sampling_rate, | |
| mid_sil=200, | |
| lead_sil=100, | |
| trail_sil=200, | |
| ) | |
| if ref_wav.shape[-1] == 0: | |
| raise ValueError( | |
| "Reference audio is empty after silence removal. " | |
| "Try setting preprocess_prompt=False." | |
| ) | |
| ref_duration = ref_wav.shape[-1] / self.sampling_rate | |
| if ref_duration > 20.0: | |
| logger.warning( | |
| "Reference audio is %.1fs long (>20s). This may cause slower " | |
| "generation, higher memory usage, and degraded voice cloning " | |
| "quality. We recommend trimming it to 3-10s.", | |
| ref_duration, | |
| ) | |
| if ref_text is None: | |
| if self._asr_pipe is None: | |
| logger.info("ASR model not loaded yet, loading on-the-fly ...") | |
| self.load_asr_model() | |
| ref_text = self.transcribe((ref_wav, self.sampling_rate)) | |
| logger.debug("Auto-transcribed ref_text: %s", ref_text) | |
| chunk_size = self.audio_tokenizer.config.hop_length | |
| clip_size = int(ref_wav.shape[-1] % chunk_size) | |
| ref_wav = ref_wav[:, :-clip_size] if clip_size > 0 else ref_wav | |
| ref_wav_tensor = torch.from_numpy(ref_wav).to(self.audio_tokenizer.device) | |
| ref_audio_tokens = self.audio_tokenizer.encode( | |
| ref_wav_tensor.unsqueeze(0), | |
| ).audio_codes.squeeze( | |
| 0 | |
| ) | |
| if preprocess_prompt: | |
| ref_text = add_punctuation(ref_text) | |
| return VoiceClonePrompt( | |
| ref_audio_tokens=ref_audio_tokens, | |
| ref_text=ref_text, | |
| ref_rms=ref_rms, | |
| ) | |
| def _decode_and_post_process( | |
| self, | |
| tokens: Union[torch.Tensor, List[torch.Tensor]], | |
| rms: Union[float, None], | |
| gen_config: iceAudioGenerationConfig, | |
| ) -> np.ndarray: | |
| tokenizer_device = self.audio_tokenizer.device | |
| if isinstance(tokens, list): | |
| chunk_audios = [ | |
| self.audio_tokenizer.decode(t.to(tokenizer_device).unsqueeze(0)) | |
| .audio_values[0] | |
| .cpu() | |
| .numpy() | |
| for t in tokens | |
| ] | |
| audio_waveform = cross_fade_chunks(chunk_audios, self.sampling_rate) | |
| else: | |
| audio_waveform = ( | |
| self.audio_tokenizer.decode(tokens.to(tokenizer_device).unsqueeze(0)) | |
| .audio_values[0] | |
| .cpu() | |
| .numpy() | |
| ) | |
| audio_waveform = self._post_process_audio( | |
| audio_waveform, | |
| postprocess_output=gen_config.postprocess_output, | |
| ref_rms=rms, | |
| ) | |
| return audio_waveform.squeeze(0) | |
| def _post_process_audio( | |
| self, | |
| generated_audio: np.ndarray, | |
| postprocess_output: bool, | |
| ref_rms: Union[float, None], | |
| ) -> np.ndarray: | |
| if postprocess_output: | |
| generated_audio = remove_silence( | |
| generated_audio, | |
| self.sampling_rate, | |
| mid_sil=500, | |
| lead_sil=100, | |
| trail_sil=100, | |
| ) | |
| if ref_rms is not None and ref_rms < 0.1: | |
| generated_audio = generated_audio * ref_rms / 0.1 | |
| elif ref_rms is None: | |
| peak = np.abs(generated_audio).max() | |
| if peak > 1e-6: | |
| generated_audio = generated_audio / peak * 0.5 | |
| generated_audio = fade_and_pad_audio( | |
| generated_audio, | |
| sample_rate=self.sampling_rate, | |
| ) | |
| return generated_audio | |
| def _generate_chunked( | |
| self, task: GenerationTask, gen_config: iceAudioGenerationConfig | |
| ) -> List[List[torch.Tensor]]: | |
| all_chunks = [] | |
| for i in range(task.batch_size): | |
| avg_tokens_per_char = task.target_lens[i] / len(task.texts[i]) | |
| text_chunk_len = int( | |
| gen_config.audio_chunk_duration | |
| * self.audio_tokenizer.config.frame_rate | |
| / avg_tokens_per_char | |
| ) | |
| chunks = chunk_text_punctuation( | |
| text=task.texts[i], | |
| chunk_len=text_chunk_len, | |
| min_chunk_len=3, | |
| ) | |
| logger.debug(f"Item {i} chunked into {len(chunks)} pieces: {chunks}") | |
| all_chunks.append(chunks) | |
| has_ref = [t is not None for t in task.ref_audio_tokens] | |
| assert all(has_ref) or not any(has_ref), ( | |
| "Chunked inference requires all items to either have or not have " | |
| "ref_audio. Mixed ref/non-ref is not supported." | |
| ) | |
| max_num_chunks = max(len(c) for c in all_chunks) | |
| chunk_results = [[] for _ in range(task.batch_size)] | |
| def _run_batch(indices, texts, ref_audios, ref_texts): | |
| speed_list = task.speed | |
| target_lens = [ | |
| self._estimate_target_tokens( | |
| texts[j], | |
| ref_texts[j], | |
| ref_audios[j].size(-1) if ref_audios[j] is not None else None, | |
| speed=speed_list[i] if speed_list else 1.0, | |
| ) | |
| for j, i in enumerate(indices) | |
| ] | |
| sub_task = GenerationTask( | |
| batch_size=len(indices), | |
| texts=texts, | |
| target_lens=target_lens, | |
| langs=[task.langs[i] for i in indices], | |
| instructs=[task.instructs[i] for i in indices], | |
| ref_texts=ref_texts, | |
| ref_audio_tokens=ref_audios, | |
| ref_rms=[task.ref_rms[i] for i in indices], | |
| speed=[task.speed[i] for i in indices] if task.speed else None, | |
| ) | |
| gen_tokens = self._generate_iterative(sub_task, gen_config) | |
| for j, idx in enumerate(indices): | |
| chunk_results[idx].append(gen_tokens[j]) | |
| if all(has_ref): | |
| for ci in range(max_num_chunks): | |
| indices = [i for i in range(task.batch_size) if ci < len(all_chunks[i])] | |
| if not indices: | |
| continue | |
| _run_batch( | |
| indices, | |
| texts=[all_chunks[i][ci] for i in indices], | |
| ref_audios=[task.ref_audio_tokens[i] for i in indices], | |
| ref_texts=[task.ref_texts[i] for i in indices], | |
| ) | |
| else: | |
| indices_0 = [i for i in range(task.batch_size) if len(all_chunks[i]) > 0] | |
| _run_batch( | |
| indices_0, | |
| texts=[all_chunks[i][0] for i in indices_0], | |
| ref_audios=[None] * len(indices_0), | |
| ref_texts=[None] * len(indices_0), | |
| ) | |
| first_chunk_map = {idx: chunk_results[idx][0] for idx in indices_0} | |
| for ci in range(1, max_num_chunks): | |
| indices = [i for i in range(task.batch_size) if ci < len(all_chunks[i])] | |
| if not indices: | |
| continue | |
| _run_batch( | |
| indices, | |
| texts=[all_chunks[i][ci] for i in indices], | |
| ref_audios=[first_chunk_map[i] for i in indices], | |
| ref_texts=[all_chunks[i][0] for i in indices], | |
| ) | |
| return chunk_results | |
| def _preprocess_all( | |
| self, | |
| text: Union[str, list[str]], | |
| language: Union[str, list[str], None] = None, | |
| ref_text: Union[str, list[str], None] = None, | |
| ref_audio: Union[ | |
| str, | |
| list[str], | |
| tuple[torch.Tensor, int], | |
| list[tuple[torch.Tensor, int]], | |
| None, | |
| ] = None, | |
| voice_clone_prompt: Union[ | |
| VoiceClonePrompt, list[VoiceClonePrompt], None | |
| ] = None, | |
| instruct: Union[str, list[str], None] = None, | |
| preprocess_prompt: bool = True, | |
| speed: Union[float, list[Optional[float]], None] = None, | |
| duration: Union[float, list[Optional[float]], None] = None, | |
| ) -> GenerationTask: | |
| if isinstance(text, str): | |
| text_list = [text] | |
| else: | |
| assert isinstance( | |
| text, list | |
| ), "text should be a string or a list of strings" | |
| text_list = text | |
| batch_size = len(text_list) | |
| language_list = self._ensure_list(language, batch_size) | |
| language_list = [_resolve_language(lang) for lang in language_list] | |
| instruct_list = self._ensure_list(instruct, batch_size) | |
| for i, s in enumerate(instruct_list): | |
| if s is None: | |
| continue | |
| use_zh = bool(text_list[i] and _ZH_RE.search(text_list[i])) | |
| instruct_list[i] = _resolve_instruct(s, use_zh=use_zh) | |
| if voice_clone_prompt is not None and ( | |
| ref_text is not None or ref_audio is not None | |
| ): | |
| logger.warning( | |
| "Both voice_clone_prompt and ref_text/ref_audio are provided. " | |
| "ref_text/ref_audio will be ignored." | |
| ) | |
| if voice_clone_prompt is None and ref_audio is not None: | |
| ref_text_list = self._ensure_list(ref_text, batch_size, auto_repeat=False) | |
| ref_audio_list = self._ensure_list(ref_audio, batch_size, auto_repeat=False) | |
| voice_clone_prompt = [] | |
| for i in range(len(ref_text_list)): | |
| voice_clone_prompt.append( | |
| self.create_voice_clone_prompt( | |
| ref_audio=ref_audio_list[i], | |
| ref_text=ref_text_list[i], | |
| preprocess_prompt=preprocess_prompt, | |
| ) | |
| ) | |
| voice_clone_prompt_list = self._ensure_list(voice_clone_prompt, batch_size) | |
| if voice_clone_prompt_list[0] is not None: | |
| ref_text_list = [vc.ref_text for vc in voice_clone_prompt_list] | |
| ref_audio_tokens_list = [ | |
| vc.ref_audio_tokens for vc in voice_clone_prompt_list | |
| ] | |
| ref_rms_list = [vc.ref_rms for vc in voice_clone_prompt_list] | |
| else: | |
| ref_text_list = [None] * batch_size | |
| ref_audio_tokens_list = [None] * batch_size | |
| ref_rms_list = [None] * batch_size | |
| if speed is not None: | |
| if isinstance(speed, (int, float)): | |
| user_speed = [float(speed)] * batch_size | |
| else: | |
| user_speed = list(speed) | |
| else: | |
| user_speed = None | |
| if duration is not None: | |
| if isinstance(duration, (int, float)): | |
| durations = [float(duration)] * batch_size | |
| else: | |
| durations = list(duration) | |
| else: | |
| durations = None | |
| num_target_tokens_list = [] | |
| for i in range(batch_size): | |
| has_dur = durations is not None and durations[i] is not None | |
| item_speed = 1.0 if has_dur else (user_speed[i] if user_speed else 1.0) | |
| est = self._estimate_target_tokens( | |
| text_list[i], | |
| ref_text_list[i], | |
| ref_audio_tokens_list[i].size(-1) | |
| if ref_audio_tokens_list[i] is not None | |
| else None, | |
| speed=item_speed, | |
| ) | |
| num_target_tokens_list.append(est) | |
| speed_list: Optional[List[float]] = None | |
| if durations is not None: | |
| frame_rate = self.audio_tokenizer.config.frame_rate | |
| speed_list = [] | |
| for i in range(batch_size): | |
| if durations[i] is not None: | |
| target_tokens = max(1, int(durations[i] * frame_rate)) | |
| est = num_target_tokens_list[i] | |
| speed_list.append(est / target_tokens if target_tokens > 0 else 1.0) | |
| num_target_tokens_list[i] = target_tokens | |
| else: | |
| s = user_speed[i] if user_speed else None | |
| speed_list.append(s if s is not None else 1.0) | |
| elif user_speed is not None: | |
| speed_list = [s if s is not None else 1.0 for s in user_speed] | |
| return GenerationTask( | |
| batch_size=batch_size, | |
| texts=text_list, | |
| target_lens=num_target_tokens_list, | |
| langs=language_list, | |
| instructs=instruct_list, | |
| ref_texts=ref_text_list, | |
| ref_audio_tokens=ref_audio_tokens_list, | |
| ref_rms=ref_rms_list, | |
| speed=speed_list, | |
| ) | |
| def _estimate_target_tokens(self, text, ref_text, num_ref_audio_tokens, speed=1.0): | |
| if num_ref_audio_tokens is None or ref_text is None or len(ref_text) == 0: | |
| ref_text = "Nice to meet you." | |
| num_ref_audio_tokens = 25 | |
| est = self.duration_estimator.estimate_duration( | |
| text, ref_text, num_ref_audio_tokens | |
| ) | |
| if speed > 0 and speed != 1.0: | |
| est = est / speed | |
| return max(1, int(est)) | |
| def _ensure_list( | |
| self, x: Union[Any, List[Any]], batch_size: int, auto_repeat: bool = True | |
| ) -> List[Any]: | |
| x_list = x if isinstance(x, list) else [x] | |
| if len(x_list) not in ( | |
| 1, | |
| batch_size, | |
| ): | |
| raise ValueError( | |
| f"should be either the number of the text or 1, but got {len(x_list)}" | |
| ) | |
| if auto_repeat and len(x_list) == 1 and batch_size is not None: | |
| x_list = x_list * batch_size | |
| return x_list | |
| def _prepare_inference_inputs( | |
| self, | |
| text: str, | |
| num_target_tokens: int, | |
| ref_text: Optional[str] = None, | |
| ref_audio_tokens: Optional[torch.Tensor] = None, | |
| lang: Optional[str] = None, | |
| instruct: Optional[str] = None, | |
| denoise: bool = True, | |
| ): | |
| style_text = "" | |
| legacy_runtime = getattr(self,'runtime_mode','independent_training') == 'legacy_audio_compatible' | |
| if denoise and ref_audio_tokens is not None: | |
| style_text += "<|denoise|>" if legacy_runtime else "<|ice_r_instruction|>denoise<|ice_r_text_end|>" | |
| lang_str = lang if lang else "None" | |
| instruct_str = instruct if instruct else "None" | |
| if legacy_runtime: | |
| style_text += f"<|lang_start|>{lang_str}<|lang_end|>" | |
| style_text += f"<|instruct_start|>{instruct_str}<|instruct_end|>" | |
| else: | |
| style_text += f"<|ice_r_lang|>{lang_str}<|ice_r_text_end|>" | |
| style_text += f"<|ice_r_instruction|>{instruct_str}<|ice_r_text_end|>" | |
| style_tokens = ( | |
| self.text_tokenizer(style_text, return_tensors="pt") | |
| .input_ids.repeat(self.config.num_audio_codebook, 1) | |
| .unsqueeze(0) | |
| ).to( | |
| self.device | |
| ) | |
| full_text = _combine_text(ref_text=ref_text, text=text) | |
| wrapped_text = f"<|text_start|>{full_text}<|text_end|>" if legacy_runtime else f"<|ice_r_text|>{full_text}<|ice_r_text_end|>" | |
| text_tokens = ( | |
| _tokenize_with_nonverbal_tags(wrapped_text, self.text_tokenizer) | |
| .repeat(self.config.num_audio_codebook, 1) | |
| .unsqueeze(0) | |
| ).to( | |
| self.device | |
| ) | |
| target_audio_tokens = torch.full( | |
| (1, self.config.num_audio_codebook, num_target_tokens), | |
| self.config.audio_mask_id, | |
| dtype=torch.long, | |
| device=self.device, | |
| ) | |
| parts = [style_tokens, text_tokens] | |
| if ref_audio_tokens is not None: | |
| parts.append(ref_audio_tokens.unsqueeze(0).to(self.device)) | |
| parts.append(target_audio_tokens) | |
| cond_input_ids = torch.cat(parts, dim=2) | |
| cond_total_length = cond_input_ids.shape[2] | |
| cond_audio_start_idx = cond_total_length - num_target_tokens | |
| if ref_audio_tokens is not None: | |
| cond_audio_start_idx -= ref_audio_tokens.size(-1) | |
| cond_audio_mask = torch.zeros( | |
| 1, cond_total_length, dtype=torch.bool, device=self.device | |
| ) | |
| cond_audio_mask[0, cond_audio_start_idx:] = True | |
| return { | |
| "input_ids": cond_input_ids, | |
| "audio_mask": cond_audio_mask, | |
| } | |
| def _generate_iterative( | |
| self, task: GenerationTask, gen_config: iceAudioGenerationConfig | |
| ) -> List[torch.Tensor]: | |
| B = task.batch_size | |
| for i in range(B): | |
| logger.debug( | |
| "Item %d — text: %s | ref_text: %s | instruct: %s | lang: %s | target_tokens: %d", | |
| i, | |
| task.texts[i], | |
| task.ref_texts[i], | |
| task.instructs[i], | |
| task.langs[i], | |
| task.target_lens[i], | |
| ) | |
| inputs_list = [ | |
| self._prepare_inference_inputs( | |
| task.texts[i], | |
| task.target_lens[i], | |
| task.ref_texts[i], | |
| task.ref_audio_tokens[i], | |
| task.langs[i], | |
| task.instructs[i], | |
| gen_config.denoise, | |
| ) | |
| for i in range(B) | |
| ] | |
| c_lens = [inp["input_ids"].size(2) for inp in inputs_list] | |
| max_c_len = max(c_lens) | |
| pad_id = self.config.audio_mask_id | |
| batch_input_ids = torch.full( | |
| (2 * B, self.config.num_audio_codebook, max_c_len), | |
| pad_id, | |
| dtype=torch.long, | |
| device=self.device, | |
| ) | |
| batch_audio_mask = torch.zeros( | |
| (2 * B, max_c_len), dtype=torch.bool, device=self.device | |
| ) | |
| batch_attention_mask = torch.zeros( | |
| (2 * B, 1, max_c_len, max_c_len), dtype=torch.bool, device=self.device | |
| ) | |
| for i, inp in enumerate(inputs_list): | |
| c_len, u_len = c_lens[i], task.target_lens[i] | |
| batch_input_ids[i, :, :c_len] = inp["input_ids"] | |
| batch_audio_mask[i, :c_len] = inp["audio_mask"] | |
| batch_attention_mask[i, :, :c_len, :c_len] = True | |
| batch_input_ids[B + i, :, :u_len] = inp["input_ids"][..., -u_len:] | |
| batch_audio_mask[B + i, :u_len] = inp["audio_mask"][..., -u_len:] | |
| batch_attention_mask[B + i, :, :u_len, :u_len] = True | |
| if max_c_len > u_len: | |
| pad_diag = torch.arange(u_len, max_c_len, device=self.device) | |
| batch_attention_mask[B + i, :, pad_diag, pad_diag] = True | |
| tokens = torch.full( | |
| (B, self.config.num_audio_codebook, max(task.target_lens)), | |
| self.config.audio_mask_id, | |
| dtype=torch.long, | |
| device=self.device, | |
| ) | |
| timesteps = _get_time_steps( | |
| t_start=0.0, | |
| t_end=1.0, | |
| num_step=gen_config.num_step, | |
| t_shift=gen_config.t_shift, | |
| ).tolist() | |
| schedules = [] | |
| for t_len in task.target_lens: | |
| total_mask = t_len * self.config.num_audio_codebook | |
| rem = total_mask | |
| sched = [] | |
| for step in range(gen_config.num_step): | |
| num = ( | |
| rem | |
| if step == gen_config.num_step - 1 | |
| else min( | |
| math.ceil(total_mask * (timesteps[step + 1] - timesteps[step])), | |
| rem, | |
| ) | |
| ) | |
| sched.append(int(num)) | |
| rem -= int(num) | |
| schedules.append(sched) | |
| layer_ids = torch.arange( | |
| self.config.num_audio_codebook, device=self.device | |
| ).view(1, -1, 1) | |
| for step in range(gen_config.num_step): | |
| batch_logits = self( | |
| input_ids=batch_input_ids, | |
| audio_mask=batch_audio_mask, | |
| attention_mask=batch_attention_mask, | |
| ).logits.to(torch.float32) | |
| for i in range(B): | |
| k = schedules[i][step] | |
| if k <= 0: | |
| continue | |
| c_len, t_len = c_lens[i], task.target_lens[i] | |
| c_logits = batch_logits[i : i + 1, :, c_len - t_len : c_len, :] | |
| u_logits = batch_logits[B + i : B + i + 1, :, :t_len, :] | |
| pred_tokens, scores = self._predict_tokens_with_scoring( | |
| c_logits, u_logits, gen_config | |
| ) | |
| scores = scores - (layer_ids * gen_config.layer_penalty_factor) | |
| if gen_config.position_temperature > 0.0: | |
| scores = _gumbel_sample(scores, gen_config.position_temperature) | |
| sample_tokens = tokens[i : i + 1, :, :t_len] | |
| scores.masked_fill_( | |
| sample_tokens != self.config.audio_mask_id, -float("inf") | |
| ) | |
| _, topk_idx = torch.topk(scores.flatten(), k) | |
| flat_tokens = sample_tokens.flatten() | |
| flat_tokens[topk_idx] = pred_tokens.flatten()[topk_idx] | |
| sample_tokens.copy_(flat_tokens.view_as(sample_tokens)) | |
| tokens[i : i + 1, :, :t_len] = sample_tokens | |
| batch_input_ids[i : i + 1, :, c_len - t_len : c_len] = sample_tokens | |
| batch_input_ids[B + i : B + i + 1, :, :t_len] = sample_tokens | |
| return [tokens[i, :, : task.target_lens[i]] for i in range(B)] | |
| def _predict_tokens_with_scoring(self, c_logits, u_logits, gen_config): | |
| if gen_config.guidance_scale != 0: | |
| c_log_probs = F.log_softmax(c_logits, dim=-1) | |
| u_log_probs = F.log_softmax(u_logits, dim=-1) | |
| log_probs = torch.log_softmax( | |
| c_log_probs + gen_config.guidance_scale * (c_log_probs - u_log_probs), | |
| dim=-1, | |
| ) | |
| else: | |
| log_probs = F.log_softmax(c_logits, dim=-1) | |
| log_probs[..., self.config.audio_mask_id] = -float("inf") | |
| if gen_config.class_temperature > 0.0: | |
| filtered_probs = _filter_top_k(log_probs, ratio=0.1) | |
| pred_tokens = _gumbel_sample( | |
| filtered_probs, gen_config.class_temperature | |
| ).argmax(dim=-1) | |
| else: | |
| pred_tokens = log_probs.argmax(dim=-1) | |
| confidence_scores = log_probs.max(dim=-1)[0] | |
| return pred_tokens, confidence_scores | |
| class iceAudioForCausalLM(iceAudio): | |
| config_class = iceAudioConfig | |
| def _get_packed_mask(document_ids): | |
| return partial(_mask_mod_packed, document_ids) | |
| def _mask_mod_packed(document_ids, b, h, q_idx, kv_idx): | |
| same_doc = document_ids[q_idx] == document_ids[kv_idx] | |
| return same_doc | |
| def _resolve_language(language: Optional[str]) -> Union[str, None]: | |
| if language is None or language.lower() == "none": | |
| return None | |
| if language in LANG_IDS: | |
| return language | |
| key = language.lower() | |
| if key in LANG_NAME_TO_ID: | |
| return LANG_NAME_TO_ID[key] | |
| raise ValueError( | |
| f"Unsupported language '{language}'. ICE-R supports exactly the 590 entries " | |
| "in languages.json; unsupported values are rejected explicitly." | |
| ) | |
| def _resolve_instruct( | |
| instruct: Optional[str], use_zh: bool = False | |
| ) -> Union[str, None]: | |
| if instruct is None: | |
| return None | |
| instruct_str = instruct.strip() | |
| if not instruct_str: | |
| return None | |
| raw_items = re.split(r"\s*[,,]\s*", instruct_str) | |
| raw_items = [x for x in raw_items if x] | |
| unknown = [] | |
| normalised = [] | |
| for raw in raw_items: | |
| n = raw.strip().lower() | |
| if n in _INSTRUCT_ALL_VALID: | |
| normalised.append(n) | |
| else: | |
| sug = difflib.get_close_matches(n, _INSTRUCT_ALL_VALID, n=1, cutoff=0.6) | |
| unknown.append((raw, n, sug[0] if sug else None)) | |
| if unknown: | |
| lines = [] | |
| for raw, n, sug in unknown: | |
| if sug: | |
| lines.append(f" '{raw}' -> '{n}' (unsupported; did you mean '{sug}'?)") | |
| else: | |
| lines.append(f" '{raw}' -> '{n}' (unsupported)") | |
| err = ( | |
| f"Unsupported instruct items found in {instruct_str}:\n" | |
| + "\n".join(lines) | |
| + "\n\nValid English items: " | |
| + ", ".join(sorted(_INSTRUCT_VALID_EN)) | |
| + "\nValid Chinese items: " | |
| + ",".join(sorted(_INSTRUCT_VALID_ZH)) | |
| + "\n\nTip: Use only English or only Chinese instructs. " | |
| "English instructs should use comma + space (e.g. " | |
| "'male, indian accent'),\nChinese instructs should use full-width " | |
| "comma (e.g. '男,河南话')." | |
| ) | |
| raise ValueError(err) | |
| has_dialect = any(n.endswith("话") for n in normalised) | |
| has_accent = any(" accent" in n for n in normalised) | |
| if has_dialect and has_accent: | |
| raise ValueError( | |
| "Cannot mix Chinese dialect and English accent in a single instruct. " | |
| "Dialects are for Chinese speech, accents for English speech." | |
| ) | |
| if has_dialect: | |
| use_zh = True | |
| elif has_accent: | |
| use_zh = False | |
| if use_zh: | |
| normalised = [_INSTRUCT_EN_TO_ZH.get(n, n) for n in normalised] | |
| else: | |
| normalised = [_INSTRUCT_ZH_TO_EN.get(n, n) for n in normalised] | |
| conflicts = [] | |
| for cat in _INSTRUCT_MUTUALLY_EXCLUSIVE: | |
| hits = [n for n in normalised if n in cat] | |
| if len(hits) > 1: | |
| conflicts.append(hits) | |
| if conflicts: | |
| parts = [] | |
| for group in conflicts: | |
| parts.append(" vs ".join(f"'{x}'" for x in group)) | |
| raise ValueError( | |
| "Conflicting instruct items within the same category: " | |
| + "; ".join(parts) | |
| + ". Each category (gender, age, pitch, style, accent, dialect) " | |
| "allows at most one item." | |
| ) | |
| has_zh = any(any("\u4e00" <= c <= "\u9fff" for c in n) for n in normalised) | |
| separator = "," if has_zh else ", " | |
| return separator.join(normalised) | |
| def _filter_top_k(logits: torch.Tensor, ratio: float = 0.1) -> torch.Tensor: | |
| k = math.ceil(ratio * logits.shape[-1]) | |
| val, ind = logits.topk(k, dim=-1) | |
| probs = torch.full_like(logits, float("-inf")) | |
| probs.scatter_(-1, ind, val) | |
| return probs | |
| def _gumbel_sample(logits: torch.Tensor, temperature: float) -> torch.Tensor: | |
| scaled_logits = logits / temperature | |
| u = torch.rand_like(scaled_logits) | |
| gumbel_noise = -torch.log(-torch.log(u + 1e-10) + 1e-10) | |
| return scaled_logits + gumbel_noise | |
| def _get_time_steps( | |
| t_start: float = 0.0, | |
| t_end: float = 1.0, | |
| num_step: int = 10, | |
| t_shift: float = 1.0, | |
| device: torch.device = torch.device("cpu"), | |
| ) -> torch.Tensor: | |
| timesteps = torch.linspace(t_start, t_end, num_step + 1).to(device) | |
| timesteps = t_shift * timesteps / (1 + (t_shift - 1) * timesteps) | |
| return timesteps | |
| _NONVERBAL_PATTERN = re.compile( | |
| r"\[(laughter|sigh|confirmation-en|question-en|question-ah|question-oh|" | |
| r"question-ei|question-yi|surprise-ah|surprise-oh|surprise-wa|" | |
| r"surprise-yo|dissatisfaction-hnn)\]" | |
| ) | |
| def _tokenize_with_nonverbal_tags(text: str, tokenizer) -> torch.Tensor: | |
| parts = [] | |
| last_end = 0 | |
| for m in _NONVERBAL_PATTERN.finditer(text): | |
| if m.start() > last_end: | |
| segment = text[last_end : m.start()] | |
| ids = tokenizer(segment, add_special_tokens=False).input_ids | |
| if ids: | |
| parts.append(ids) | |
| tag_ids = tokenizer(m.group(), add_special_tokens=False).input_ids | |
| if tag_ids: | |
| parts.append(tag_ids) | |
| last_end = m.end() | |
| if last_end < len(text): | |
| segment = text[last_end:] | |
| ids = tokenizer(segment, add_special_tokens=False).input_ids | |
| if ids: | |
| parts.append(ids) | |
| if not parts: | |
| result = tokenizer(text, return_tensors="pt").input_ids | |
| else: | |
| combined = [] | |
| for p in parts: | |
| combined.extend(p) | |
| result = torch.tensor([combined], dtype=torch.long) | |
| return result | |
| def _combine_text(text, ref_text: Optional[str] = None) -> str: | |
| if ref_text: | |
| full_text = ref_text.strip() + " " + text.strip() | |
| else: | |
| full_text = text.strip() | |
| full_text = re.sub(r"[\r\n]+", "", full_text) | |
| full_text = full_text.replace("\uff08", "(").replace("\uff09", ")") | |
| full_text = re.sub(r"[ \t]+", " ", full_text) | |
| chinese_range = r"[\u4e00-\u9fff]" | |
| pattern = rf"(?<={chinese_range})\s+|\s+(?={chinese_range})" | |
| full_text = re.sub(pattern, "", full_text) | |
| return full_text | |
| AutoConfig.register("ice-012-audio", iceAudioConfig, exist_ok=True) | |
| AutoModel.register(iceAudioConfig, iceAudio) | |
| AutoModelForCausalLM.register(iceAudioConfig, iceAudioForCausalLM) | |
| class FunctionPreservingCapacity(nn.Module): | |
| def __init__(self, hidden_size=1024, expert_width=7560, experts=6): | |
| super().__init__() | |
| self.experts = nn.ModuleList([ | |
| nn.Sequential(nn.Linear(hidden_size, expert_width, bias=False), nn.SiLU(), nn.Linear(expert_width, hidden_size, bias=False)) | |
| for _ in range(experts) | |
| ]) | |
| self.routing_memory = nn.Parameter(torch.zeros(8102719)) | |
| self.output_gate = nn.Parameter(torch.zeros(1)) | |
| for e in self.experts: | |
| nn.init.normal_(e[0].weight, mean=0.0, std=0.02) | |
| nn.init.zeros_(e[2].weight) | |
| def forward(self, x): | |
| if self.output_gate.detach().abs().item() == 0.0: | |
| return x | |
| y=sum(e(x) for e in self.experts) / len(self.experts) | |
| route=self.routing_memory.mean().to(dtype=x.dtype, device=x.device) | |
| return x + self.output_gate.to(dtype=x.dtype) * (y + route) | |
| class IceRLanguageModel(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| self.config=config | |
| self.model=IndependentBackbone(config) | |
| def get_input_embeddings(self): return self.model.get_input_embeddings() | |
| def set_input_embeddings(self,value): self.model.embed=value | |
| def forward(self,input_ids=None,inputs_embeds=None,attention_mask=None,return_dict=True,position_ids=None,**kwargs): | |
| return self.model(input_ids=input_ids,inputs_embeds=inputs_embeds,attention_mask=attention_mask,position_ids=position_ids,return_dict=True) | |
| class iceReengineeredAudioForCausalLM(iceAudioForCausalLM): | |
| config_class = iceReengineeredAudioConfig | |
| def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs): | |
| cfg = kwargs.get('config') | |
| if cfg is None: | |
| from transformers import AutoConfig | |
| cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, trust_remote_code=True, local_files_only=kwargs.get('local_files_only', False)) | |
| nested = getattr(cfg, 'language_model_config', None) | |
| if isinstance(nested, dict): | |
| cfg.language_model_config = iceAudioLanguageConfig(**nested) | |
| kwargs['config'] = cfg | |
| return super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs) | |
| def __init__(self, config, *args, **kwargs): | |
| super().__init__(config, *args, **kwargs) | |
| self.runtime_mode=getattr(config,'runtime_mode','independent_training') | |
| if self.runtime_mode != 'legacy_audio_compatible': | |
| self.language_model=IceRLanguageModel(config.language_model_config) | |
| self.architecture_version='ice-r/3.1.0' | |
| def _prepare_embed_inputs(self, input_ids, audio_mask): | |
| return super()._prepare_embed_inputs(input_ids, audio_mask) | |
| def supported_language_ids(self): | |
| return set(LANG_IDS) | |
| def supported_language_names(self): | |
| return set(LANG_NAMES) | |