techfreakworm commited on
Commit
2a9234b
·
unverified ·
1 Parent(s): 49b81d4

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).

Files changed (5) hide show
  1. qvs/audio.py +17 -3
  2. qvs/device.py +5 -19
  3. qvs/engine.py +3 -1
  4. qvs/lora.py +10 -4
  5. 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 concat(wavs: list[np.ndarray], sr: int, gap_s: float = 0.15) -> np.ndarray:
40
- """Join chunk waveforms with a short silence between them (long-form)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- """flash_attention_2 on CUDA when the wheel is present, else sdpa.
67
-
68
- flash-attn is CUDA-only, so MPS always uses sdpa. sdpa is a safe default on
69
- CUDA too (just a little slower than flash-attn).
70
  """
71
- dev = device or target_device()
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
- _SENT_SPLIT = re.compile(r"(?<=[.!?。!?…])\s+")
 
 
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
- obj = torch.load(path, map_location="cpu", weights_only=False)
 
 
 
 
67
  if isinstance(obj, dict):
 
 
68
  for v in obj.values():
69
- import torch as _t
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
- VOICE_DIR = os.environ.get("QVS_VOICE_DIR", "voice_library")
 
 
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=False)
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")