Download code/stlm/audio_model.py from IndexTeam/Index-Echo-S2ST-9B: direct link, hf CLI and curl.
- Browser
- Download file 5.79 kB
-
https://huggingface.co/IndexTeam/Index-Echo-S2ST-9B/resolve/main/code/stlm/audio_model.py
- Command line
-
hf download hf://IndexTeam/Index-Echo-S2ST-9B/code/stlm/audio_model.py
-
curl -L -o audio_model.py https://huggingface.co/IndexTeam/Index-Echo-S2ST-9B/resolve/main/code/stlm/audio_model.py
5.79 kB
| """组合模型 9B 版 (导出包 vendored): Qwen3-Omni AuT(2048) + connector(2048→4096 投影) + Qwen3.5-9B (early fusion)。 | |
| 与 audio-trans work/audio_model_9b.py 逻辑一致, 路径全部参数化 | |
| (llm_path / omni_path 由调用方传入, 不再硬编码 $AT)。 | |
| 架构: wav → WhisperFeatureExtractor(128 mel) → AuT(2048) → connector(2048→4096) → | |
| 替换 <|audio_pad|> 位。connector = 列正交 W × gain (EMBED_RMS_9B/AUT_RMS/√(2048/4096), | |
| 输出 per-elem RMS ≈ 9B embed 行 RMS 0.0137); log_alpha 已删 (2026-09-11 极简口径)。 | |
| 类名保持 Qwen35AudioForCausalLM 与 2B 包一致, pipeline 无需区分 2B/9B。 | |
| """ | |
| import json | |
| import os | |
| import torch | |
| import torch.nn as nn | |
| AUDIO_PAD = 248076 # <|audio_pad|> | |
| AUDIO_DIM = 2048 # AuT output_dim | |
| EMBED_RMS_9B = 0.0137 # 实测 9B 基座 embed 行均值 | |
| AUT_RMS = 0.0143 # 实测 AuT 输出帧 RMS | |
| class AudioConnector9B(nn.Module): | |
| """h' = W·h; W: 2048→4096 列正交 × gain。""" | |
| def __init__(self, in_dim=AUDIO_DIM, out_dim=4096, | |
| gain=EMBED_RMS_9B / AUT_RMS / (AUDIO_DIM / 4096) ** 0.5): | |
| super().__init__() | |
| self.proj = nn.Linear(in_dim, out_dim, bias=False) | |
| nn.init.orthogonal_(self.proj.weight) # out>in → 列正交, 保范数 | |
| self.proj.weight.data *= gain | |
| def forward(self, h): | |
| return self.proj(h) | |
| def load_audio_tower(omni_path, dtype=torch.bfloat16): | |
| """从 Qwen3-Omni shard 1 加载 AuT(525 keys)。omni_path: 含 config.json + | |
| model-00001-of-00015.safetensors 的目录 (导出包 stlm_omni/)。""" | |
| from transformers.models.qwen3_omni_moe.modeling_qwen3_omni_moe import ( | |
| Qwen3OmniMoeAudioEncoder, Qwen3OmniMoeAudioEncoderConfig) | |
| from safetensors.torch import load_file | |
| cfg = json.load(open(os.path.join(omni_path, 'config.json'))) | |
| audio_cfg = Qwen3OmniMoeAudioEncoderConfig(**{k: v for k, v in | |
| cfg['thinker_config']['audio_config'].items() | |
| if not k.startswith('_')}) | |
| enc = Qwen3OmniMoeAudioEncoder(audio_cfg).eval() | |
| sd = load_file(os.path.join(omni_path, 'model-00001-of-00015.safetensors')) | |
| sd = {k.replace('thinker.audio_tower.', ''): v for k, v in sd.items() | |
| if k.startswith('thinker.audio_tower.')} | |
| missing, unexpected = enc.load_state_dict(sd, strict=False) | |
| assert not missing and not unexpected, f'missing={missing[:3]} unexpected={unexpected[:3]}' | |
| return enc.to(dtype) | |
| def load_feature_extractor(): | |
| from transformers import WhisperFeatureExtractor | |
| fe = WhisperFeatureExtractor(feature_size=128, hop_length=160, n_fft=400, | |
| sampling_rate=16000, padding_value=0.0, | |
| return_attention_mask=True) | |
| # 构造器忽略 n_samples; 必须事后设(默认 480000=30s 会截断 60s 语料) | |
| fe.n_samples = 4800000 | |
| fe.nb_max_frames = 30000 | |
| return fe | |
| class Qwen35AudioForCausalLM(nn.Module): | |
| """AuT + connector + Qwen3.5-9B, audio embedding 原位替换 <|audio_pad|>。 | |
| (类名与 2B 包相同; 9B 差异: LLM 4096h/32L untied, connector 为 2048→4096 投影) | |
| """ | |
| def __init__(self, llm_path, omni_path, dtype=torch.bfloat16, | |
| freeze_llm=False, freeze_audio=False): | |
| super().__init__() | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| self.tokenizer = AutoTokenizer.from_pretrained(llm_path) | |
| self.llm = AutoModelForCausalLM.from_pretrained(llm_path, dtype=dtype) | |
| self.audio_tower = load_audio_tower(omni_path, dtype) | |
| self.fe = load_feature_extractor() | |
| self.connector = AudioConnector9B(out_dim=self.llm.config.hidden_size).to(dtype) | |
| assert self.llm.config.hidden_size == 4096 | |
| assert self.llm.config.tie_word_embeddings is False | |
| if freeze_llm: | |
| for p in self.llm.parameters(): | |
| p.requires_grad_(False) | |
| if freeze_audio: | |
| for p in self.audio_tower.parameters(): | |
| p.requires_grad_(False) | |
| def device(self): | |
| return next(self.llm.parameters()).device | |
| def encode_audio(self, wav_paths): | |
| """wav paths → list of (T_i, 4096) connector 后 audio embeddings + 各段帧数。""" | |
| import librosa | |
| embs = [] | |
| dt = next(self.audio_tower.parameters()).dtype | |
| for p in wav_paths: | |
| wav, _ = librosa.load(p, sr=16000) | |
| f = self.fe(wav, sampling_rate=16000, return_tensors='pt', | |
| return_attention_mask=True) | |
| T = int(f.attention_mask.sum(-1)[0]) | |
| feats = f.input_features[0][:, :T] | |
| out = self.audio_tower(input_features=feats.to(self.device, dt), | |
| feature_lens=torch.tensor([T], device=self.device)) | |
| h = out.last_hidden_state if hasattr(out, 'last_hidden_state') else out[0] | |
| embs.append(self.connector(h.to(self.device, dt))) | |
| return embs | |
| def forward(self, input_ids, attention_mask=None, labels=None, | |
| audio_embs=None, **kwargs): | |
| embed = self.llm.get_input_embeddings() | |
| inputs_embeds = embed(input_ids) | |
| if audio_embs is not None: | |
| pad_mask = input_ids == AUDIO_PAD | |
| n_pad = int(pad_mask.sum()) | |
| n_audio = sum(e.shape[0] for e in audio_embs) | |
| assert n_pad == n_audio, f'pad tokens {n_pad} != audio frames {n_audio}' | |
| inputs_embeds = inputs_embeds.clone() | |
| inputs_embeds[pad_mask] = torch.cat(audio_embs).to(inputs_embeds.dtype) | |
| return self.llm(inputs_embeds=inputs_embeds, attention_mask=attention_mask, | |
| labels=labels, **kwargs) | |