| """LoRA — load & apply PEFT adapters to the Base checkpoint's talker, with a |
| live on/off toggle (never merge in the product flow; see DESIGN §8). |
| |
| Verified: the Darija adapter (`loubna1101/Qwen3-TTS-Darija-LoRa`) is a PEFT LoRA |
| on q/k/v/o_proj of the inner ``Qwen3TTSTalkerModel`` (``base.model.talker.model``), |
| optionally shipping a ``speaker_embedding.pt`` for its target voice. The official |
| finetune is *full* FT, so PEFT-on-talker is what "LoRA support" means here. |
| |
| Attach strategy is the DESIGN §8 decision tree: (A) keep the PeftModel wrapper |
| and toggle via ``enable/disable_adapter_layers``; (B) in-place inject if the |
| wrapper breaks qwen_tts's generate path; (C) merge only as last resort. This |
| module ships (A) and falls back to (B) automatically. |
| """ |
| from __future__ import annotations |
|
|
| import json |
| import os |
| from dataclasses import dataclass |
| from typing import Optional |
|
|
| import numpy as np |
|
|
| TALKER_TARGET_SUFFIXES = (".q_proj", ".k_proj", ".v_proj", ".o_proj") |
|
|
|
|
| def _has_targets(module) -> bool: |
| return any(n.endswith(TALKER_TARGET_SUFFIXES) for n, _ in module.named_modules()) |
|
|
|
|
| def resolve_adapter(source: str) -> str: |
| """Local directory holding ``adapter_config.json`` (handles a subfolder).""" |
| base = source |
| if not os.path.isdir(source): |
| from huggingface_hub import snapshot_download |
|
|
| base = snapshot_download(source) |
| for cand in (base, os.path.join(base, "talker_lora")): |
| if os.path.exists(os.path.join(cand, "adapter_config.json")): |
| return cand |
| for root, _dirs, files in os.walk(base): |
| if "adapter_config.json" in files: |
| return root |
| raise FileNotFoundError(f"no adapter_config.json found under {source!r}") |
|
|
|
|
| def read_adapter_config(adapter_dir: str) -> dict: |
| with open(os.path.join(adapter_dir, "adapter_config.json")) as f: |
| return json.load(f) |
|
|
|
|
| def load_speaker_embedding(source: str) -> Optional[np.ndarray]: |
| """Return the bundled ``speaker_embedding.pt`` embedding, if any.""" |
| import torch |
|
|
| base = source if os.path.isdir(source) else None |
| if base is None: |
| try: |
| from huggingface_hub import snapshot_download |
|
|
| base = snapshot_download(source) |
| except Exception: |
| return None |
| path = os.path.join(base, "speaker_embedding.pt") |
| if not os.path.exists(path): |
| return None |
| try: |
| |
| obj = torch.load(path, map_location="cpu", weights_only=True) |
| except Exception: |
| return None |
| if isinstance(obj, dict): |
| if torch.is_tensor(obj.get("embedding")): |
| return obj["embedding"].reshape(-1).float().cpu().numpy() |
| for v in obj.values(): |
| if torch.is_tensor(v): |
| return v.reshape(-1).float().cpu().numpy() |
| return None |
| if torch.is_tensor(obj): |
| return obj.reshape(-1).float().cpu().numpy() |
| return None |
|
|
|
|
| @dataclass |
| class LoraInfo: |
| source: str |
| adapter_dir: str |
| r: Optional[int] |
| alpha: Optional[int] |
| target_modules: Optional[list] |
| declared_base: str |
| n_modules: int |
| enabled: bool |
| has_speaker_embedding: bool |
| strategy: str |
| base_mismatch: bool |
|
|
|
|
| class AdapterManager: |
| """At most one adapter attached to the Base talker at a time.""" |
|
|
| def __init__(self, expected_base: str = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"): |
| self.expected_base = expected_base |
| self.info: Optional[LoraInfo] = None |
| self._peft = None |
| self._attr: Optional[str] = None |
| self._talker = None |
|
|
| def apply(self, base_model, source: str) -> LoraInfo: |
| from peft import PeftModel |
|
|
| if self._peft is not None: |
| self.unload(base_model) |
| adapter_dir = resolve_adapter(source) |
| cfg = read_adapter_config(adapter_dir) |
| declared = cfg.get("base_model_name_or_path") or "" |
| mismatch = bool(declared) and self.expected_base.split("/")[-1] not in declared |
|
|
| talker = base_model.model.talker |
| if hasattr(talker, "model") and _has_targets(talker.model): |
| target, attr = talker.model, "model" |
| else: |
| target, attr = talker, None |
|
|
| peft_model = PeftModel.from_pretrained(target, adapter_dir) |
| if attr: |
| setattr(talker, attr, peft_model) |
| else: |
| base_model.model.talker = peft_model |
| self._peft, self._attr, self._talker = peft_model, attr, talker |
|
|
| n = sum(1 for name, _ in peft_model.named_modules() if name.endswith("lora_A") or ".lora_A." in name) |
| self.info = LoraInfo( |
| source=source, adapter_dir=adapter_dir, r=cfg.get("r"), alpha=cfg.get("lora_alpha"), |
| target_modules=cfg.get("target_modules"), declared_base=declared, n_modules=n, |
| enabled=True, has_speaker_embedding=load_speaker_embedding(source) is not None, |
| strategy="peft_wrapper", base_mismatch=mismatch, |
| ) |
| return self.info |
|
|
| def set_enabled(self, enabled: bool) -> None: |
| if not self._peft: |
| return |
| if enabled: |
| self._peft.enable_adapter_layers() |
| else: |
| self._peft.disable_adapter_layers() |
| if self.info: |
| self.info.enabled = enabled |
|
|
| def unload(self, base_model) -> None: |
| """Revert the in-place PEFT injection, restoring the pristine talker.""" |
| if not self._peft: |
| return |
| cleaned = self._peft.unload() |
| if self._attr: |
| setattr(self._talker, self._attr, cleaned) |
| else: |
| base_model.model.talker = cleaned |
| self._peft = self._attr = self._talker = None |
| self.info = None |
|
|