voidful commited on
Commit
f769cb7
·
1 Parent(s): 744bf7a

Stop earlier without clipping final speech

Browse files
Files changed (4) hide show
  1. README.md +5 -4
  2. app.py +8 -4
  3. production.py +16 -2
  4. tests/test_production.py +15 -1
README.md CHANGED
@@ -36,8 +36,8 @@ Demo 預設採用目前通過長文穩定性評估的推論設定:
36
  | CFG | 2.0 |
37
  | NFE steps | 10 |
38
  | Target pace | 4.0 speech units/sec |
39
- | Stop policy | 0.65 → 0.35 near endpoint, 2 consecutive hits |
40
- | Hard stop | predicted target steps |
41
  | Pace correction | pitch-preserving stretch after natural completion; first chunk maximum 24 chars |
42
  | Maximum chunk | 80 chars |
43
  | Minimum chunk | 12 chars |
@@ -50,8 +50,9 @@ Demo 預設採用目前通過長文穩定性評估的推論設定:
50
  並依逗號、分號或句末標點插入不同長度的停頓。輸出最後會套用保守的 RMS floor 與 peak limit。
51
 
52
  自然 stop 在目前 checkpoint 上仍可能過快或錯過句尾。Demo 不再用最低生成長度強迫模型繼續
53
- 發聲;模型自然完成文字後才做保音高語速校正,接近預期 endpoint 時降低 stop threshold,並以
54
- 預估長度上界阻止多餘尾音。進階參數可供研究比較,但 release profile 是 CFG 2.0、NFE 10、後處理語速 1.0。
 
55
 
56
  請只使用已取得授權的參考音檔。合成語音僅供研究與評估展示,正式使用前請人工檢視。
57
 
 
36
  | CFG | 2.0 |
37
  | NFE steps | 10 |
38
  | Target pace | 4.0 speech units/sec |
39
+ | Stop policy | 0.65 → 0.20 from 65% to 90% predicted progress, 2 consecutive hits |
40
+ | Hard stop | predicted target steps + 1 step |
41
  | Pace correction | pitch-preserving stretch after natural completion; first chunk maximum 24 chars |
42
  | Maximum chunk | 80 chars |
43
  | Minimum chunk | 12 chars |
 
50
  並依逗號、分號或句末標點插入不同長度的停頓。輸出最後會套用保守的 RMS floor 與 peak limit。
51
 
52
  自然 stop 在目前 checkpoint 上仍可能過快或錯過句尾。Demo 不再用最低生成長度強迫模型繼續
53
+ 發聲;模型自然完成文字後才做保音高語速校正,並在預估進度 65% 後逐步降低 stop threshold。
54
+ 最後保留一個 latent step 的收尾空間,再以硬上界阻止多餘尾音。進階參數可供研究比較,但 release
55
+ profile 是 CFG 2.0、NFE 10、後處理語速 1.0。
56
 
57
  請只使用已取得授權的參考音檔。合成語音僅供研究與評估展示,正式使用前請人工檢視。
58
 
app.py CHANGED
@@ -58,10 +58,12 @@ CROSSFADE_MS = 80.0
58
  CHUNK_EDGE_FADE_MS = 80.0
59
  CHUNK_RMS_MATCH_DB = 4.0
60
  STOP_THRESHOLD = 0.65
61
- STOP_LATE_THRESHOLD = 0.35
 
 
62
  STOP_CONSECUTIVE = 2
63
  HARD_STOP_RATIO = 1.0
64
- HARD_STOP_MARGIN_STEPS = 0
65
  MIN_PACE_SPEED = 0.80
66
  MAX_TEXT_CHARS = 360
67
 
@@ -116,6 +118,8 @@ if not _NATIVE_STOP_POLICY:
116
  threshold=STOP_THRESHOLD,
117
  late_threshold=STOP_LATE_THRESHOLD,
118
  consecutive=STOP_CONSECUTIVE,
 
 
119
  )
120
  model.stop_head = _STOP_CONTROLLER
121
 
@@ -187,7 +191,7 @@ def _generate_chunk(
187
  "retry_badcase_ratio_threshold": 6.0,
188
  }
189
  if _NATIVE_STOP_POLICY:
190
- kwargs["stop_threshold"] = STOP_LATE_THRESHOLD
191
  kwargs["stop_consecutive"] = STOP_CONSECUTIVE
192
  if "generation_seed" in _GENERATE_PARAMETERS:
193
  kwargs["generation_seed"] = request_seed
@@ -347,7 +351,7 @@ HEADER = f"""
347
  台灣華語與中英混合文字轉語音。模型版本:`{CHECKPOINT}`。
348
 
349
  目前預設採用穩定推論設定:`CFG 2.0`、`NFE 10`、目標語速 `4.0 字/秒`、
350
- 生成完成後校正至目標語速、動態 stop hysteresis、target-length hard stop、每 80 字切段,且不使用 retry 或 rerank。
351
  """
352
 
353
 
 
58
  CHUNK_EDGE_FADE_MS = 80.0
59
  CHUNK_RMS_MATCH_DB = 4.0
60
  STOP_THRESHOLD = 0.65
61
+ STOP_LATE_THRESHOLD = 0.20
62
+ STOP_LATE_START_RATIO = 0.65
63
+ STOP_LATE_FULL_RATIO = 0.90
64
  STOP_CONSECUTIVE = 2
65
  HARD_STOP_RATIO = 1.0
66
+ HARD_STOP_MARGIN_STEPS = 1
67
  MIN_PACE_SPEED = 0.80
68
  MAX_TEXT_CHARS = 360
69
 
 
118
  threshold=STOP_THRESHOLD,
119
  late_threshold=STOP_LATE_THRESHOLD,
120
  consecutive=STOP_CONSECUTIVE,
121
+ late_start_ratio=STOP_LATE_START_RATIO,
122
+ late_full_ratio=STOP_LATE_FULL_RATIO,
123
  )
124
  model.stop_head = _STOP_CONTROLLER
125
 
 
191
  "retry_badcase_ratio_threshold": 6.0,
192
  }
193
  if _NATIVE_STOP_POLICY:
194
+ kwargs["stop_threshold"] = STOP_THRESHOLD
195
  kwargs["stop_consecutive"] = STOP_CONSECUTIVE
196
  if "generation_seed" in _GENERATE_PARAMETERS:
197
  kwargs["generation_seed"] = request_seed
 
351
  台灣華語與中英混合文字轉語音。模型版本:`{CHECKPOINT}`。
352
 
353
  目前預設採用穩定推論設定:`CFG 2.0`、`NFE 10`、目標語速 `4.0 字/秒`、
354
+ 生成完成後校正至目標語速、提前降低 stop threshold、target + 1 step hard stop、每 80 字切段,且不使用 retry 或 rerank。
355
  """
356
 
357
 
production.py CHANGED
@@ -42,12 +42,19 @@ class StopHysteresisController(nn.Module):
42
  threshold: float = 0.65,
43
  late_threshold: float = 0.50,
44
  consecutive: int = 2,
 
 
45
  ) -> None:
46
  super().__init__()
47
  self.stop_head = stop_head
48
  self.threshold = float(threshold)
49
  self.late_threshold = float(late_threshold)
50
  self.consecutive = max(1, int(consecutive))
 
 
 
 
 
51
  self._active = False
52
  self._min_len = 2
53
  self._expected_steps = 0
@@ -82,8 +89,15 @@ class StopHysteresisController(nn.Module):
82
  threshold = self.threshold
83
  if self._expected_steps > 0:
84
  progress = generated_steps / float(self._expected_steps)
85
- if progress >= 0.8:
86
- blend = min(1.0, max(0.0, (progress - 0.8) / 0.2))
 
 
 
 
 
 
 
87
  threshold = self.threshold + blend * (self.late_threshold - self.threshold)
88
  eligible = self._step > self._min_len
89
  if eligible and probability >= threshold:
 
42
  threshold: float = 0.65,
43
  late_threshold: float = 0.50,
44
  consecutive: int = 2,
45
+ late_start_ratio: float = 0.80,
46
+ late_full_ratio: float = 1.00,
47
  ) -> None:
48
  super().__init__()
49
  self.stop_head = stop_head
50
  self.threshold = float(threshold)
51
  self.late_threshold = float(late_threshold)
52
  self.consecutive = max(1, int(consecutive))
53
+ self.late_start_ratio = max(0.0, float(late_start_ratio))
54
+ self.late_full_ratio = max(
55
+ self.late_start_ratio + 1.0e-6,
56
+ float(late_full_ratio),
57
+ )
58
  self._active = False
59
  self._min_len = 2
60
  self._expected_steps = 0
 
89
  threshold = self.threshold
90
  if self._expected_steps > 0:
91
  progress = generated_steps / float(self._expected_steps)
92
+ if progress >= self.late_start_ratio:
93
+ blend = min(
94
+ 1.0,
95
+ max(
96
+ 0.0,
97
+ (progress - self.late_start_ratio)
98
+ / (self.late_full_ratio - self.late_start_ratio),
99
+ ),
100
+ )
101
  threshold = self.threshold + blend * (self.late_threshold - self.threshold)
102
  eligible = self._step > self._min_len
103
  if eligible and probability >= threshold:
tests/test_production.py CHANGED
@@ -60,6 +60,20 @@ def test_stop_controller_relaxes_near_endpoint_and_enforces_hard_stop():
60
  assert decisions == [0, 0, 0, 1]
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def test_split_preserves_punctuation_and_minimum_chunk_size():
64
  text = (
65
  "第一句很短。第二句也不長。第三句需要再多一些文字,才能測試切段是否正確。"
@@ -82,7 +96,7 @@ def test_duration_units_and_target_pace_min_len():
82
  stop_consecutive=2,
83
  ) == 5
84
  assert target_cps_steps("一二三四五六七八", target_cps=4.0, step_seconds=0.25) == 8
85
- assert duration_hard_stop_steps(55, ratio=1.0, margin_steps=0) == 55
86
  assert duration_hard_stop_steps(0, ratio=1.08, margin_steps=3) == 2000
87
 
88
 
 
60
  assert decisions == [0, 0, 0, 1]
61
 
62
 
63
+ def test_stop_controller_can_relax_before_the_text_pace_cap():
64
+ controller = StopHysteresisController(
65
+ _SequenceStopHead([0.4] * 9),
66
+ threshold=0.65,
67
+ late_threshold=0.20,
68
+ consecutive=2,
69
+ late_start_ratio=0.65,
70
+ late_full_ratio=0.90,
71
+ )
72
+ controller.begin(min_len=2, expected_steps=10, hard_stop_steps=11)
73
+ decisions = [controller(torch.zeros(1, 2)).argmax(dim=-1).item() for _ in range(9)]
74
+ assert decisions == [0, 0, 0, 0, 0, 0, 0, 0, 1]
75
+
76
+
77
  def test_split_preserves_punctuation_and_minimum_chunk_size():
78
  text = (
79
  "第一句很短。第二句也不長。第三句需要再多一些文字,才能測試切段是否正確。"
 
96
  stop_consecutive=2,
97
  ) == 5
98
  assert target_cps_steps("一二三四五六七八", target_cps=4.0, step_seconds=0.25) == 8
99
+ assert duration_hard_stop_steps(55, ratio=1.0, margin_steps=1) == 56
100
  assert duration_hard_stop_steps(0, ratio=1.08, margin_steps=3) == 2000
101
 
102