elitexp commited on
Commit
d296d19
·
verified ·
1 Parent(s): 80be62c

Trim sentence edges; sentence_pause_ms (default 350 ms)

Browse files
Files changed (2) hide show
  1. README.md +1 -0
  2. swar/inference.py +36 -13
README.md CHANGED
@@ -69,6 +69,7 @@ python -m swar.inference "नमस्ते, यहाँलाई स्वा
69
  | `voice` | a name from `voices/` (default `bijaya`), a `.pt` of style vectors, or an audio clip (wav / flac / mp3 / ogg) |
70
  | `speed` | 0.5-2.0, scales the predicted durations (1.0 = as predicted) |
71
  | `pause_floor_ms` | every comma / full stop / question mark *inside* a sentence lasts at least this long (0 = as predicted) |
 
72
  | `return_info` | also return timing and the exact letters the model read |
73
 
74
  ## What is inside
 
69
  | `voice` | a name from `voices/` (default `bijaya`), a `.pt` of style vectors, or an audio clip (wav / flac / mp3 / ogg) |
70
  | `speed` | 0.5-2.0, scales the predicted durations (1.0 = as predicted) |
71
  | `pause_floor_ms` | every comma / full stop / question mark *inside* a sentence lasts at least this long (0 = as predicted) |
72
+ | `sentence_pause_ms` | silence between two sentences (default 350); the start and end of each sentence are trimmed, so the first sound comes within about 0.1 s |
73
  | `return_info` | also return timing and the exact letters the model read |
74
 
75
  ## What is inside
swar/inference.py CHANGED
@@ -4,7 +4,7 @@
4
  tts = Swar.from_pretrained("elitexp/Swar") # or a local folder
5
  wav, sr = tts.synthesize("नमस्ते, यहाँलाई स्वागत छ।") # default voice: bijaya
6
  wav, sr = tts.synthesize("Your balance is Rs. 250.", voice="path/to/reference.wav",
7
- speed=1.0, pause_floor_ms=300)
8
 
9
  python -m swar.inference "नमस्ते, यहाँलाई स्वागत छ।" -o hello.wav
10
 
@@ -13,6 +13,12 @@ prosodic (manner of speaking, read by the duration / pitch / energy predictor).
13
  voices/*.pt hold them ready-made; any clean 3-10 s clip of one speaker gives a new
14
  voice through style_from_audio(). pause_floor_ms makes every comma / full stop /
15
  question mark inside a sentence last at least that long.
 
 
 
 
 
 
16
  """
17
  from __future__ import annotations
18
 
@@ -29,7 +35,9 @@ from .model import build, conf
29
  from .text import STOI, normalize, to_ids
30
 
31
  PAUSE_CHARS = ",.?!;:…—"
32
- GAP_S = 0.15 # silence between sentences
 
 
33
  MAX_CHARS = 220
34
 
35
 
@@ -65,6 +73,7 @@ class Swar:
65
  self.m = m.to(self.device).eval()
66
  self.mel = Mel().to(self.device)
67
  self._pause_ids = torch.tensor([STOI[c] for c in PAUSE_CHARS], device=self.device)
 
68
  self._voices: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
69
 
70
  @classmethod
@@ -106,7 +115,10 @@ class Swar:
106
 
107
  # ---- synthesis
108
  @torch.no_grad()
109
- def _tts(self, ids: list[int], s_ac, s_pr, speed: float, pause_floor: int) -> torch.Tensor:
 
 
 
110
  m, dev = self.m, self.device
111
  x_ids = torch.tensor([ids], device=dev)
112
  n = torch.tensor([len(ids)], device=dev)
@@ -121,30 +133,39 @@ class Swar:
121
  pos = torch.arange(L, device=dev)
122
  p = torch.isin(x_ids[0], self._pause_ids) & (pos > 0) & (pos < L - 2) # inside the sentence
123
  dur[0, p] = dur[0, p].clamp(min=pause_floor)
 
 
 
 
124
  aln = _alignment(dur, int(dur.sum()))
125
  F0, N = m["predictor"].F0Ntrain(d.transpose(-1, -2) @ aln, s_pr)
126
  asr = m["text_encoder"](x_ids, n.cpu(), mask) @ aln
127
  return m["decoder"](asr, F0, N, s_ac).squeeze().clamp(-1, 1)
128
 
129
  def synthesize(self, text: str, voice: str | Path | None = None, speed: float = 1.0,
130
- pause_floor_ms: int = 0, return_info: bool = False):
131
  """-> (float32 waveform, 16000), plus a timing dict if return_info."""
132
  t0 = time.time()
133
  s_ac, s_pr = self.voice(voice)
134
  floor = int(round(pause_floor_ms / 25))
135
  speed = float(min(2.0, max(0.5, speed)))
136
- pieces, sentences, first = [], [], None
137
  # split the RAW text first, so each sentence's numbers are read in its own
138
  # language ("Rs. 250" stays together: no split before a digit)
139
  raws = re.split(r"(?<=[।?!])\s+|(?<=\.)\s+(?![0-9०-९])", text)
140
- for sent in (s for raw in raws for s in split_sentences(normalize(raw))):
141
- sentences.append(sent)
142
- y = self._tts(to_ids(sent), s_ac, s_pr, speed, floor).float().cpu().numpy()
143
- first = first if first is not None else time.time() - t0
144
- pieces += [y, np.zeros(int(GAP_S * SR), np.float32)]
145
- if not pieces:
146
  raise ValueError("nothing speakable left after normalising the text")
147
- wav = np.concatenate(pieces[:-1])
 
 
 
 
 
 
 
 
 
148
  if not return_info:
149
  return wav, SR
150
  return wav, SR, {"first_audio_sec": round(first, 3), "total_sec": round(time.time() - t0, 3),
@@ -159,12 +180,14 @@ def main() -> int:
159
  ap.add_argument("--voice", default=None, help="voice name, .pt, or a reference audio clip")
160
  ap.add_argument("--speed", type=float, default=1.0)
161
  ap.add_argument("--pause-floor-ms", type=int, default=0)
 
162
  ap.add_argument("--device", default=None)
163
  a = ap.parse_args()
164
  import soundfile as sf
165
  tts = Swar.from_pretrained(a.model, device=a.device)
166
  wav, sr, info = tts.synthesize(a.text, voice=a.voice, speed=a.speed,
167
- pause_floor_ms=a.pause_floor_ms, return_info=True)
 
168
  sf.write(a.out, wav, sr)
169
  print(f"{a.out}: {info['duration_sec']} s audio in {info['total_sec']} s -- read as: "
170
  f"{' | '.join(info['sentences'])}")
 
4
  tts = Swar.from_pretrained("elitexp/Swar") # or a local folder
5
  wav, sr = tts.synthesize("नमस्ते, यहाँलाई स्वागत छ।") # default voice: bijaya
6
  wav, sr = tts.synthesize("Your balance is Rs. 250.", voice="path/to/reference.wav",
7
+ speed=1.0, pause_floor_ms=300, sentence_pause_ms=350)
8
 
9
  python -m swar.inference "नमस्ते, यहाँलाई स्वागत छ।" -o hello.wav
10
 
 
13
  voices/*.pt hold them ready-made; any clean 3-10 s clip of one speaker gives a new
14
  voice through style_from_audio(). pause_floor_ms makes every comma / full stop /
15
  question mark inside a sentence last at least that long.
16
+
17
+ Sentences are generated one at a time. Training clips began and ended with room
18
+ silence, so the model gives the start and end tokens 0.3-0.5 s each; joined
19
+ sentence by sentence that piled up to ~1 s at every full stop and delayed the first
20
+ sound. So the edges are cut to EDGE_UNITS, and the gap between two sentences is
21
+ exactly sentence_pause_ms, carried by the first sentence's final punctuation.
22
  """
23
  from __future__ import annotations
24
 
 
35
  from .text import STOI, normalize, to_ids
36
 
37
  PAUSE_CHARS = ",.?!;:…—"
38
+ UNIT_S = 0.025 # one duration unit
39
+ EDGE_UNITS = 2 # start / end token cap (50 ms)
40
+ TAIL_UNITS = 4 # after the last sentence (100 ms)
41
  MAX_CHARS = 220
42
 
43
 
 
73
  self.m = m.to(self.device).eval()
74
  self.mel = Mel().to(self.device)
75
  self._pause_ids = torch.tensor([STOI[c] for c in PAUSE_CHARS], device=self.device)
76
+ self._pause_set = {STOI[c] for c in PAUSE_CHARS}
77
  self._voices: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
78
 
79
  @classmethod
 
115
 
116
  # ---- synthesis
117
  @torch.no_grad()
118
+ def _tts(self, ids: list[int], s_ac, s_pr, speed: float, pause_floor: int,
119
+ end_pause: int | None = None, tail: int = EDGE_UNITS) -> torch.Tensor:
120
+ """end_pause: units for the sentence-final punctuation (None = as predicted); the
121
+ last sentence gets a short one, so the clip does not end in half a second of nothing."""
122
  m, dev = self.m, self.device
123
  x_ids = torch.tensor([ids], device=dev)
124
  n = torch.tensor([len(ids)], device=dev)
 
133
  pos = torch.arange(L, device=dev)
134
  p = torch.isin(x_ids[0], self._pause_ids) & (pos > 0) & (pos < L - 2) # inside the sentence
135
  dur[0, p] = dur[0, p].clamp(min=pause_floor)
136
+ dur[0, 0] = dur[0, 0].clamp(max=EDGE_UNITS) # room silence before the sentence
137
+ dur[0, -1] = dur[0, -1].clamp(max=tail) # ... and after it
138
+ if end_pause is not None and ids[-2] in self._pause_set:
139
+ dur[0, -2] = max(1, end_pause)
140
  aln = _alignment(dur, int(dur.sum()))
141
  F0, N = m["predictor"].F0Ntrain(d.transpose(-1, -2) @ aln, s_pr)
142
  asr = m["text_encoder"](x_ids, n.cpu(), mask) @ aln
143
  return m["decoder"](asr, F0, N, s_ac).squeeze().clamp(-1, 1)
144
 
145
  def synthesize(self, text: str, voice: str | Path | None = None, speed: float = 1.0,
146
+ pause_floor_ms: int = 0, sentence_pause_ms: int = 350, return_info: bool = False):
147
  """-> (float32 waveform, 16000), plus a timing dict if return_info."""
148
  t0 = time.time()
149
  s_ac, s_pr = self.voice(voice)
150
  floor = int(round(pause_floor_ms / 25))
151
  speed = float(min(2.0, max(0.5, speed)))
152
+ gap = int(round(max(0, sentence_pause_ms) / 1000 / UNIT_S)) - 2 * EDGE_UNITS # edges are silent too
153
  # split the RAW text first, so each sentence's numbers are read in its own
154
  # language ("Rs. 250" stays together: no split before a digit)
155
  raws = re.split(r"(?<=[।?!])\s+|(?<=\.)\s+(?![0-9०-९])", text)
156
+ sentences = [s for raw in raws for s in split_sentences(normalize(raw))]
157
+ if not sentences:
 
 
 
 
158
  raise ValueError("nothing speakable left after normalising the text")
159
+ pieces, first = [], None
160
+ for k, sent in enumerate(sentences):
161
+ last = k == len(sentences) - 1
162
+ y = self._tts(to_ids(sent), s_ac, s_pr, speed, floor, end_pause=TAIL_UNITS if last else gap,
163
+ tail=TAIL_UNITS if last else EDGE_UNITS).float().cpu().numpy()
164
+ first = first if first is not None else time.time() - t0
165
+ pieces.append(y)
166
+ if not last and sent[-1] not in PAUSE_CHARS: # cut mid run-on: pause as silence
167
+ pieces.append(np.zeros(int(max(0, gap) * UNIT_S * SR), np.float32))
168
+ wav = np.concatenate(pieces)
169
  if not return_info:
170
  return wav, SR
171
  return wav, SR, {"first_audio_sec": round(first, 3), "total_sec": round(time.time() - t0, 3),
 
180
  ap.add_argument("--voice", default=None, help="voice name, .pt, or a reference audio clip")
181
  ap.add_argument("--speed", type=float, default=1.0)
182
  ap.add_argument("--pause-floor-ms", type=int, default=0)
183
+ ap.add_argument("--sentence-pause-ms", type=int, default=350)
184
  ap.add_argument("--device", default=None)
185
  a = ap.parse_args()
186
  import soundfile as sf
187
  tts = Swar.from_pretrained(a.model, device=a.device)
188
  wav, sr, info = tts.synthesize(a.text, voice=a.voice, speed=a.speed,
189
+ pause_floor_ms=a.pause_floor_ms, sentence_pause_ms=a.sentence_pause_ms,
190
+ return_info=True)
191
  sf.write(a.out, wav, sr)
192
  print(f"{a.out}: {info['duration_sec']} s audio in {info['total_sec']} s -- read as: "
193
  f"{' | '.join(info['sentences'])}")