Security + correctness hardening (brain code review)
Browse files- SECURITY: torch.load(weights_only=True) for adapter speaker_embedding.pt and
saved voices — never pickle-execute arbitrary user-supplied HF payloads.
- CJK long-form: sentence splitter now \s* (was \s+, never split 。!?).
- Click-free long-form joins: short edge fades on each chunk before the gap.
- sdpa everywhere; dropped flash-attn auto-select (wrong sm_120 kernels risk).
- LoRA double-apply guard (no nested PeftModel wrappers).
- Voice library under data/ (gitignored; clone prompts are biometric-adjacent).
- qvs/audio.py +17 -3
- qvs/device.py +5 -19
- qvs/engine.py +3 -1
- qvs/lora.py +10 -4
- qvs/voices.py +4 -2
qvs/audio.py
CHANGED
|
@@ -36,8 +36,22 @@ def ref_from_gradio(audio) -> Optional[tuple[np.ndarray, int]]:
|
|
| 36 |
return None
|
| 37 |
|
| 38 |
|
| 39 |
-
def
|
| 40 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
if not wavs:
|
| 42 |
return np.zeros(0, dtype=np.float32)
|
| 43 |
if len(wavs) == 1:
|
|
@@ -45,7 +59,7 @@ def concat(wavs: list[np.ndarray], sr: int, gap_s: float = 0.15) -> np.ndarray:
|
|
| 45 |
gap = np.zeros(int(sr * gap_s), dtype=np.float32)
|
| 46 |
out: list[np.ndarray] = []
|
| 47 |
for i, w in enumerate(wavs):
|
| 48 |
-
out.append(np.asarray(w, dtype=np.float32))
|
| 49 |
if i != len(wavs) - 1:
|
| 50 |
out.append(gap)
|
| 51 |
return np.concatenate(out)
|
|
|
|
| 36 |
return None
|
| 37 |
|
| 38 |
|
| 39 |
+
def _edge_fade(w: np.ndarray, sr: int, ms: float = 8.0) -> np.ndarray:
|
| 40 |
+
"""Linear fade-in/out on the chunk edges so joins into the silence gap don't
|
| 41 |
+
click (a hard cut from a non-zero sample is a step discontinuity)."""
|
| 42 |
+
n = min(int(sr * ms / 1000.0), len(w) // 2)
|
| 43 |
+
if n <= 0:
|
| 44 |
+
return w
|
| 45 |
+
w = w.astype(np.float32, copy=True)
|
| 46 |
+
ramp = np.linspace(0.0, 1.0, n, dtype=np.float32)
|
| 47 |
+
w[:n] *= ramp
|
| 48 |
+
w[-n:] *= ramp[::-1]
|
| 49 |
+
return w
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def concat(wavs: list[np.ndarray], sr: int, gap_s: float = 0.12, fade_ms: float = 8.0) -> np.ndarray:
|
| 53 |
+
"""Join chunk waveforms with a short silence between them (long-form),
|
| 54 |
+
edge-fading each chunk so the joins are click-free."""
|
| 55 |
if not wavs:
|
| 56 |
return np.zeros(0, dtype=np.float32)
|
| 57 |
if len(wavs) == 1:
|
|
|
|
| 59 |
gap = np.zeros(int(sr * gap_s), dtype=np.float32)
|
| 60 |
out: list[np.ndarray] = []
|
| 61 |
for i, w in enumerate(wavs):
|
| 62 |
+
out.append(_edge_fade(np.asarray(w, dtype=np.float32), sr, fade_ms))
|
| 63 |
if i != len(wavs) - 1:
|
| 64 |
out.append(gap)
|
| 65 |
return np.concatenate(out)
|
qvs/device.py
CHANGED
|
@@ -9,7 +9,6 @@ path used for reference audio.
|
|
| 9 |
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
-
import functools
|
| 13 |
import os
|
| 14 |
|
| 15 |
|
|
@@ -52,26 +51,13 @@ def get_dtype():
|
|
| 52 |
return torch.bfloat16
|
| 53 |
|
| 54 |
|
| 55 |
-
@functools.lru_cache(maxsize=1)
|
| 56 |
-
def _flash_attn_available() -> bool:
|
| 57 |
-
try:
|
| 58 |
-
import flash_attn # noqa: F401
|
| 59 |
-
|
| 60 |
-
return True
|
| 61 |
-
except Exception:
|
| 62 |
-
return False
|
| 63 |
-
|
| 64 |
-
|
| 65 |
def get_attn_impl(device: str | None = None) -> str:
|
| 66 |
-
"""
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
"""
|
| 71 |
-
|
| 72 |
-
if dev == "cuda" and _flash_attn_available():
|
| 73 |
-
return "flash_attention_2"
|
| 74 |
-
return "sdpa"
|
| 75 |
|
| 76 |
|
| 77 |
# ---- optional ZeroGPU decorator ----------------------------------------------
|
|
|
|
| 9 |
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
| 12 |
import os
|
| 13 |
|
| 14 |
|
|
|
|
| 51 |
return torch.bfloat16
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def get_attn_impl(device: str | None = None) -> str:
|
| 55 |
+
"""sdpa everywhere (DESIGN D7). flash-attn is deliberately NOT auto-selected:
|
| 56 |
+
its import can succeed pre-fork on CUDA while the sm_120 (Blackwell) kernels
|
| 57 |
+
are wrong/untested — a silent-corruption risk for zero fidelity gain. Override
|
| 58 |
+
only via the explicit QVS_ATTN env for future experiments.
|
| 59 |
"""
|
| 60 |
+
return os.environ.get("QVS_ATTN", "sdpa")
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
# ---- optional ZeroGPU decorator ----------------------------------------------
|
qvs/engine.py
CHANGED
|
@@ -93,7 +93,9 @@ def free_cache() -> None:
|
|
| 93 |
|
| 94 |
|
| 95 |
# ---- long-form chunking -------------------------------------------------------
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
def split_sentences(text: str) -> list[str]:
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
# ---- long-form chunking -------------------------------------------------------
|
| 96 |
+
# \s* (not \s+): CJK sentences have no whitespace after 。!?, so \s+ would never
|
| 97 |
+
# split them and long-form would collapse to one chunk.
|
| 98 |
+
_SENT_SPLIT = re.compile(r"(?<=[.!?。!?…])\s*")
|
| 99 |
|
| 100 |
|
| 101 |
def split_sentences(text: str) -> list[str]:
|
qvs/lora.py
CHANGED
|
@@ -63,12 +63,16 @@ def load_speaker_embedding(source: str) -> Optional[np.ndarray]:
|
|
| 63 |
path = os.path.join(base, "speaker_embedding.pt")
|
| 64 |
if not os.path.exists(path):
|
| 65 |
return None
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
if isinstance(obj, dict):
|
|
|
|
|
|
|
| 68 |
for v in obj.values():
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
if _t.is_tensor(v):
|
| 72 |
return v.reshape(-1).float().cpu().numpy()
|
| 73 |
return None
|
| 74 |
if torch.is_tensor(obj):
|
|
@@ -104,6 +108,8 @@ class AdapterManager:
|
|
| 104 |
def apply(self, base_model, source: str) -> LoraInfo:
|
| 105 |
from peft import PeftModel
|
| 106 |
|
|
|
|
|
|
|
| 107 |
adapter_dir = resolve_adapter(source)
|
| 108 |
cfg = read_adapter_config(adapter_dir)
|
| 109 |
declared = cfg.get("base_model_name_or_path") or ""
|
|
|
|
| 63 |
path = os.path.join(base, "speaker_embedding.pt")
|
| 64 |
if not os.path.exists(path):
|
| 65 |
return None
|
| 66 |
+
try:
|
| 67 |
+
# weights_only=True: never pickle-execute an arbitrary user-supplied repo.
|
| 68 |
+
obj = torch.load(path, map_location="cpu", weights_only=True)
|
| 69 |
+
except Exception:
|
| 70 |
+
return None # refuse rather than fall back to unsafe loading
|
| 71 |
if isinstance(obj, dict):
|
| 72 |
+
if torch.is_tensor(obj.get("embedding")):
|
| 73 |
+
return obj["embedding"].reshape(-1).float().cpu().numpy()
|
| 74 |
for v in obj.values():
|
| 75 |
+
if torch.is_tensor(v):
|
|
|
|
|
|
|
| 76 |
return v.reshape(-1).float().cpu().numpy()
|
| 77 |
return None
|
| 78 |
if torch.is_tensor(obj):
|
|
|
|
| 108 |
def apply(self, base_model, source: str) -> LoraInfo:
|
| 109 |
from peft import PeftModel
|
| 110 |
|
| 111 |
+
if self._peft is not None: # never nest PeftModel wrappers — clear any prior adapter first
|
| 112 |
+
self.unload(base_model)
|
| 113 |
adapter_dir = resolve_adapter(source)
|
| 114 |
cfg = read_adapter_config(adapter_dir)
|
| 115 |
declared = cfg.get("base_model_name_or_path") or ""
|
qvs/voices.py
CHANGED
|
@@ -13,7 +13,9 @@ from typing import Optional
|
|
| 13 |
|
| 14 |
import numpy as np
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
def _ensure_dir() -> str:
|
|
@@ -80,7 +82,7 @@ def load_voice(name: str):
|
|
| 80 |
import torch
|
| 81 |
from qwen_tts import VoiceClonePromptItem
|
| 82 |
|
| 83 |
-
payload = torch.load(_path(name), map_location="cpu", weights_only=
|
| 84 |
items = []
|
| 85 |
for d in payload["items"]:
|
| 86 |
ref_code = d.get("ref_code")
|
|
|
|
| 13 |
|
| 14 |
import numpy as np
|
| 15 |
|
| 16 |
+
# Under data/ (gitignored): saved clone prompts are biometric-adjacent and must
|
| 17 |
+
# never be committed to the public repo.
|
| 18 |
+
VOICE_DIR = os.environ.get("QVS_VOICE_DIR", "data/voices")
|
| 19 |
|
| 20 |
|
| 21 |
def _ensure_dir() -> str:
|
|
|
|
| 82 |
import torch
|
| 83 |
from qwen_tts import VoiceClonePromptItem
|
| 84 |
|
| 85 |
+
payload = torch.load(_path(name), map_location="cpu", weights_only=True)
|
| 86 |
items = []
|
| 87 |
for d in payload["items"]:
|
| 88 |
ref_code = d.get("ref_code")
|