#!/usr/bin/env python3 """Graft a Whisper encoder onto Emhotob-50M via the stock Qwen2Audio shell. Qwen2AudioConfig declares `sub_configs = {"audio_config": AutoConfig, "text_config": AutoConfig}`, which is the same property that let the vision project compose a plain llama text config into Lfm2Vl with no custom modelling code. So there is nothing to write here beyond the graft itself: `generate()`, the KV cache, variable-length audio masking and `save_pretrained` all come for free. Two things make this fit better than the hand-rolled scatter the plan allowed for: * `Qwen2AudioEncoder` is structurally a `WhisperEncoder` -- identical conv1/conv2/embed_positions/ layers/layer_norm names -- so whisper-small's encoder weights load by prefix remap alone. * `_get_feat_extract_output_lengths` + per-audio masking means variable-length audio is handled natively. Whisper pads every clip to 30s, and without this every 3-second clip would spend ~750 tokens on silence. Frame rate: whisper conv2 (stride 2) gives 50 Hz, then Qwen2Audio's avg_pooler (stride 2) halves it to 25 Hz. MASC's median 3.0s clip -> ~75 audio tokens, p95 7.8s -> ~195. Well inside 2048. """ from __future__ import annotations import argparse from pathlib import Path import torch from transformers import (AutoConfig, AutoModelForCausalLM, AutoTokenizer, Qwen2AudioConfig, Qwen2AudioEncoderConfig, Qwen2AudioForConditionalGeneration, WhisperConfig, WhisperForConditionalGeneration) WHISPER = "openai/whisper-small" LM = "oddadmix/50M-2048-Emhotob" AUDIO_TOKEN = "