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

Swar 0.1.0: stage E release with default voice bijaya

Browse files
LICENSE ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Swar weights (swar.safetensors, voices/) and Swar code: Creative Commons Attribution-NonCommercial
2
+ 4.0 International (CC BY-NC 4.0), https://creativecommons.org/licenses/by-nc/4.0/legalcode
3
+
4
+ swar/kokoro/: vendored from Kokoro-82M (hexgrad), Apache License 2.0 -- see swar/kokoro/LICENSE.
5
+ Style encoder (swar/model.py: LearnedDownSample, ResBlk, StyleEncoder): from StyleTTS2
6
+ (yl4579/StyleTTS2), MIT License.
README.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ language:
4
+ - ne
5
+ - en
6
+ pipeline_tag: text-to-speech
7
+ library_name: pytorch
8
+ tags:
9
+ - text-to-speech
10
+ - tts
11
+ - nepali
12
+ - styletts2
13
+ - kokoro
14
+ - customer-service
15
+ ---
16
+
17
+ # Swar
18
+
19
+ **Swar** (स्वर, "voice") is a small, experimental text-to-speech model for **Nepali and
20
+ English**, including sentences that mix the two, tuned toward a calm customer-service manner
21
+ of speaking.
22
+
23
+ - **38.5 M parameters**, 16 kHz output. The architecture is derived from
24
+ [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) (StyleTTS2 family), built from its layer
25
+ classes: non-autoregressive, so a sentence is generated in one pass.
26
+ - Reads **Devanagari and Latin letters directly**: no phonemiser, no language switch.
27
+ Numbers, currency ("Rs. 250", "रु ५००"), times and phone numbers are written out in words in
28
+ the language of their sentence.
29
+ - **Default voice: `bijaya`** (`voices/bijaya.pt`). Any 3-10 s clip of one speaker gives
30
+ another voice.
31
+ - Fast: about **15x real time on a desktop CPU** with all cores, about 6x on a single thread,
32
+ and about 30x on a consumer GPU (measured on a Ryzen 9 5900X and an RTX 3090).
33
+
34
+ Try it in the Space: [Nepali Call Center Experimental - Powered by Swar](https://huggingface.co/spaces/elitexp/Nepali-Call-Center-Experimental).
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ pip install torch torchaudio transformers safetensors huggingface_hub soundfile
40
+ ```
41
+
42
+ ```python
43
+ import sys, soundfile as sf
44
+ from huggingface_hub import snapshot_download
45
+
46
+ folder = snapshot_download("elitexp/Swar")
47
+ sys.path.insert(0, folder)
48
+ from swar import Swar
49
+
50
+ tts = Swar.from_pretrained(folder) # GPU if available, else CPU
51
+ wav, sr = tts.synthesize("नमस्ते, ग्राहक सेवा केन्द्रमा यहाँलाई स्वागत छ।") # voice "bijaya"
52
+ sf.write("hello.wav", wav, sr)
53
+
54
+ # another voice from a reference clip, a little slower, with clear pauses at punctuation
55
+ wav, sr = tts.synthesize("Your balance is Rs. 250. धन्यवाद।",
56
+ voice="my_reference.wav", speed=0.95, pause_floor_ms=300)
57
+ ```
58
+
59
+ From the command line, inside the downloaded folder:
60
+
61
+ ```bash
62
+ python -m swar.inference "नमस्ते, यहाँलाई स्वागत छ।" -o hello.wav
63
+ ```
64
+
65
+ ### Options
66
+
67
+ | argument | meaning |
68
+ |---|---|
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
75
+
76
+ | part | params | role |
77
+ |---|---|---|
78
+ | PL-BERT (ALBERT, letters) + projection | 3.5 M | contextual letter features |
79
+ | prosody predictor | 9.7 M | duration per letter, pitch and energy at 80 Hz |
80
+ | prosodic style encoder | 3.5 M | manner of speaking from a reference clip |
81
+ | text encoder | 3.2 M | letter features for the decoder |
82
+ | decoder (iSTFTNet with harmonic source) | 15.1 M | waveform at 16 kHz |
83
+ | acoustic style encoder | 3.5 M | timbre from a reference clip |
84
+
85
+ A voice is two 128-d vectors: acoustic (timbre, read by the decoder) and prosodic (manner,
86
+ read by the predictor). `voices/bijaya.pt` stores them as `{"s_ac", "s_pr"}`.
87
+
88
+ Files: `swar.safetensors` (inference weights only), `config.json`, `swar/` (model,
89
+ text front-end, mel, inference; Kokoro's layer code is vendored under `swar/kokoro/` with its
90
+ Apache-2.0 licence), `voices/`.
91
+
92
+ ## Training
93
+
94
+ Trained from scratch in five stages, after the StyleTTS2 / Kokoro recipe adapted to letters at
95
+ 16 kHz:
96
+
97
+ 1. **PL-BERT** on Nepali and English text (masked letters).
98
+ 2. **Letter aligner** (CTC) on both languages, for durations.
99
+ 3. **Acoustic model**: text encoder, decoder and both style encoders, adversarial training with
100
+ multi-period and multi-resolution discriminators (300 k steps).
101
+ 4. **Prosody**: duration, pitch and energy predictors and the prosodic style encoder (100 k steps).
102
+ 5. **Register**: a short fine-tune (6 k steps) of the prosody half on about 8 hours of
103
+ customer-service style Nepali recordings, with general data replayed so the rest of the model
104
+ does not drift.
105
+
106
+ Data (acoustic and prosody stages): about 800 hours of Nepali speech from public corpora
107
+ (including OpenSLR 43 and OpenSLR 143) and other licensed and in-house recordings, part of it
108
+ synthetic, and about 570 hours of English speech synthesised with Kokoro-82M (`af_heart`).
109
+
110
+ ## Evaluation
111
+
112
+ - **Copy-synthesis intelligibility** (stage 3, durations and pitch taken from the real clip,
113
+ recognised by the same ASR as the real recordings): Nepali character error rate through a
114
+ G.711 telephone channel 0.199 against 0.220 for the real recordings (285 clips); English word
115
+ error rate 0.097 against 0.098 (200 clips).
116
+ - **Customer-service register** (stage 5): median pitch stays within 15 Hz of the reference
117
+ voice. The pause rate (about 0.13 pauses per second, about 0.20 with `pause_floor_ms=300`)
118
+ is still below the 0.30-0.45 target band: pauses are only lengthened where the text has
119
+ punctuation.
120
+ - Full text-to-speech intelligibility has not yet been measured with the same protocol.
121
+
122
+ ## Limitations
123
+
124
+ - Experimental. Rare words, names and some English spellings can be mispronounced; Nepali
125
+ rhythm is less polished than English.
126
+ - 16 kHz, telephone-oriented; not a studio-quality voice.
127
+ - Voices from a reference clip copy that clip's recording conditions too; use clean audio.
128
+
129
+ ## Responsible use
130
+
131
+ Swar can speak in a voice taken from a short recording. Only use voices you have permission to
132
+ use, label synthetic speech as synthetic where people could mistake it for a person, and do not
133
+ use the model to impersonate anyone or to deceive.
134
+
135
+ ## Acknowledgements
136
+
137
+ Swar would not exist without **[Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M)** by
138
+ hexgrad. Its architecture is Swar's blueprint: the text encoder, the prosody predictor (duration,
139
+ pitch and energy), the iSTFTNet decoder with its harmonic source and the split acoustic / prosodic
140
+ style design all come from Kokoro, and Kokoro's own layer code is vendored in `swar/kokoro/`. Swar
141
+ re-sizes that design for 16 kHz, reads letters instead of phonemes, adds trainable style encoders
142
+ and is trained from scratch; Kokoro (voice `af_heart`) also generated Swar's English training
143
+ speech. Kokoro in turn builds on [StyleTTS2](https://github.com/yl4579/StyleTTS2) (Li et al.), whose
144
+ style encoder and PL-BERT recipe Swar also uses.
145
+
146
+ ## License
147
+
148
+ The Swar weights (`swar.safetensors`, `voices/`) and Swar's own code are released under
149
+ **[CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/)**: free for research and other
150
+ non-commercial use, with attribution. The vendored Kokoro code in `swar/kokoro/` stays under its
151
+ original Apache-2.0 licence (`swar/kokoro/LICENSE`); the StyleTTS2 style encoder code is MIT.
152
+ Training data includes [OpenSLR](https://www.openslr.org/) corpora under their own licences.
config.json ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "sample_rate": 16000,
3
+ "n_token": 256,
4
+ "plbert": {
5
+ "hidden_size": 512,
6
+ "num_attention_heads": 8,
7
+ "intermediate_size": 2048,
8
+ "max_position_embeddings": 512,
9
+ "num_hidden_layers": 12,
10
+ "dropout": 0.1
11
+ },
12
+ "hidden_dim": 384,
13
+ "text_encoder_kernel_size": 5,
14
+ "n_layer": 3,
15
+ "style_dim": 128,
16
+ "max_dur": 50,
17
+ "dropout": 0.2,
18
+ "decoder": {
19
+ "hid": 512,
20
+ "out": 256,
21
+ "res": 64
22
+ },
23
+ "istftnet": {
24
+ "upsample_rates": [
25
+ 10,
26
+ 4
27
+ ],
28
+ "upsample_kernel_sizes": [
29
+ 20,
30
+ 8
31
+ ],
32
+ "upsample_initial_channel": 256,
33
+ "resblock_kernel_sizes": [
34
+ 3,
35
+ 7,
36
+ 11
37
+ ],
38
+ "resblock_dilation_sizes": [
39
+ [
40
+ 1,
41
+ 3,
42
+ 5
43
+ ],
44
+ [
45
+ 1,
46
+ 3,
47
+ 5
48
+ ],
49
+ [
50
+ 1,
51
+ 3,
52
+ 5
53
+ ]
54
+ ],
55
+ "gen_istft_n_fft": 20,
56
+ "gen_istft_hop_size": 5
57
+ },
58
+ "style_encoder": {
59
+ "dim_in": 32,
60
+ "max_conv_dim": 256
61
+ },
62
+ "name": "swar",
63
+ "version": "0.1.0"
64
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch>=2.1
2
+ torchaudio>=2.1
3
+ transformers>=4.34
4
+ safetensors>=0.4
5
+ huggingface_hub>=0.17
6
+ soundfile>=0.12
7
+ numpy
swar.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46a6ab4469d3e36fcafee53896b23e2c3d21f843fb90c2b7037a340dcf25d285
3
+ size 154119376
swar/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Swar: a small Nepali + English text-to-speech model (Kokoro / StyleTTS2 family)."""
2
+ from .inference import Swar
3
+
4
+ __all__ = ["Swar"]
5
+ __version__ = "0.1.0"
swar/inference.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Swar inference: text (Nepali, English or mixed) -> 16 kHz speech.
2
+
3
+ from swar import Swar
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
+
11
+ A voice is two 128-d style vectors: acoustic (timbre, read by the decoder) and
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
+
19
+ import argparse
20
+ import re
21
+ import time
22
+ from pathlib import Path
23
+
24
+ import numpy as np
25
+ import torch
26
+
27
+ from .mel import SR, Mel
28
+ 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
+
36
+ def _alignment(dur: torch.Tensor, T: int) -> torch.Tensor:
37
+ end = dur.cumsum(1)
38
+ start = end - dur
39
+ t = torch.arange(T, device=dur.device)[None, None]
40
+ return ((t >= start[..., None]) & (t < end[..., None])).float()
41
+
42
+
43
+ def split_sentences(text: str) -> list[str]:
44
+ out = []
45
+ for s in re.split(r"(?<=[.?!])\s+", text):
46
+ s = s.strip()
47
+ while len(s) > MAX_CHARS: # long run-on: cut at a comma or space
48
+ cut = max(s.rfind(",", 0, MAX_CHARS), s.rfind(" ", 0, MAX_CHARS))
49
+ cut = cut if cut > 0 else MAX_CHARS
50
+ out.append(s[: cut + 1].strip())
51
+ s = s[cut + 1:].strip()
52
+ if s:
53
+ out.append(s)
54
+ return out
55
+
56
+
57
+ class Swar:
58
+ def __init__(self, folder: Path, device: str | None = None):
59
+ self.folder = Path(folder)
60
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
61
+ self.config = conf(self.folder / "config.json")
62
+ m = build("D", self.config)
63
+ from safetensors.torch import load_file
64
+ m.load_state_dict(load_file(str(self.folder / "swar.safetensors")), strict=True)
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
71
+ def from_pretrained(cls, repo_or_path: str | Path = "elitexp/Swar", device: str | None = None,
72
+ revision: str | None = None) -> "Swar":
73
+ p = Path(repo_or_path)
74
+ if not p.is_dir():
75
+ from huggingface_hub import snapshot_download
76
+ p = Path(snapshot_download(str(repo_or_path), revision=revision))
77
+ return cls(p, device)
78
+
79
+ # ---- voices
80
+ def list_voices(self) -> list[str]:
81
+ return sorted(f.stem for f in (self.folder / "voices").glob("*.pt"))
82
+
83
+ @torch.no_grad()
84
+ def style_from_audio(self, path: str | Path) -> tuple[torch.Tensor, torch.Tensor]:
85
+ import soundfile as sf
86
+ import torchaudio.functional as AF
87
+ wav, sr = sf.read(str(path), dtype="float32", always_2d=True)
88
+ x = torch.from_numpy(wav.mean(1))
89
+ if sr != SR:
90
+ x = AF.resample(x, sr, SR)
91
+ mel = self.mel(x.to(self.device)[None])[:, None]
92
+ return self.m["style_encoder"](mel), self.m["predictor_encoder"](mel)
93
+
94
+ def voice(self, voice: str | Path | None = None) -> tuple[torch.Tensor, torch.Tensor]:
95
+ """A name from voices/, a .pt of style vectors, or an audio clip."""
96
+ key = str(voice or "bijaya")
97
+ if key not in self._voices:
98
+ p = Path(key)
99
+ if p.suffix.lower() in (".wav", ".flac", ".mp3", ".ogg"):
100
+ self._voices[key] = self.style_from_audio(p)
101
+ else:
102
+ f = p if p.suffix == ".pt" else self.folder / "voices" / f"{key}.pt"
103
+ v = torch.load(f, map_location="cpu", weights_only=True)
104
+ self._voices[key] = (v["s_ac"].to(self.device), v["s_pr"].to(self.device))
105
+ return self._voices[key]
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)
113
+ mask = torch.zeros(1, len(ids), dtype=torch.bool, device=dev)
114
+ bert = m["plbert"](x_ids, attention_mask=(~mask).int()).last_hidden_state
115
+ d_en = m["bert_encoder"](bert).transpose(1, 2)
116
+ d = m["predictor"].text_encoder(d_en, s_pr, n.cpu(), mask)
117
+ h, _ = m["predictor"].lstm(d)
118
+ dur = torch.round(torch.sigmoid(m["predictor"].duration_proj(h)).sum(-1) / speed).clamp(min=1).long()
119
+ if pause_floor:
120
+ L = len(ids)
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),
151
+ "duration_sec": round(len(wav) / SR, 2), "sentences": sentences}
152
+
153
+
154
+ def main() -> int:
155
+ ap = argparse.ArgumentParser(description="Swar text-to-speech")
156
+ ap.add_argument("text")
157
+ ap.add_argument("-o", "--out", default="swar.wav")
158
+ ap.add_argument("--model", default=str(Path(__file__).resolve().parents[1]))
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'])}")
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ raise SystemExit(main())
swar/kokoro/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
swar/kokoro/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Layer classes from Kokoro-82M (hexgrad/kokoro, Apache-2.0; see LICENSE here),
2
+ vendored unchanged except for a relative import and one unused import removed."""
swar/kokoro/custom_stft.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+
6
+ class CustomSTFT(nn.Module):
7
+ """
8
+ STFT/iSTFT without unfold/complex ops, using conv1d and conv_transpose1d.
9
+
10
+ - forward STFT => Real-part conv1d + Imag-part conv1d
11
+ - inverse STFT => Real-part conv_transpose1d + Imag-part conv_transpose1d + sum
12
+ - avoids F.unfold, so easier to export to ONNX
13
+ - uses replicate or constant padding for 'center=True' to approximate 'reflect'
14
+ (reflect is not supported for dynamic shapes in ONNX)
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ filter_length=800,
20
+ hop_length=200,
21
+ win_length=800,
22
+ window="hann",
23
+ center=True,
24
+ pad_mode="replicate", # or 'constant'
25
+ ):
26
+ super().__init__()
27
+ self.filter_length = filter_length
28
+ self.hop_length = hop_length
29
+ self.win_length = win_length
30
+ self.n_fft = filter_length
31
+ self.center = center
32
+ self.pad_mode = pad_mode
33
+
34
+ # Number of frequency bins for real-valued STFT with onesided=True
35
+ self.freq_bins = self.n_fft // 2 + 1
36
+
37
+ # Build window
38
+ assert window == 'hann', window
39
+ window_tensor = torch.hann_window(win_length, periodic=True, dtype=torch.float32)
40
+ if self.win_length < self.n_fft:
41
+ # Zero-pad up to n_fft
42
+ extra = self.n_fft - self.win_length
43
+ window_tensor = F.pad(window_tensor, (0, extra))
44
+ elif self.win_length > self.n_fft:
45
+ window_tensor = window_tensor[: self.n_fft]
46
+ self.register_buffer("window", window_tensor)
47
+
48
+ # Precompute forward DFT (real, imag)
49
+ # PyTorch stft uses e^{-j 2 pi k n / N} => real=cos(...), imag=-sin(...)
50
+ n = np.arange(self.n_fft)
51
+ k = np.arange(self.freq_bins)
52
+ angle = 2 * np.pi * np.outer(k, n) / self.n_fft # shape (freq_bins, n_fft)
53
+ dft_real = np.cos(angle)
54
+ dft_imag = -np.sin(angle) # note negative sign
55
+
56
+ # Combine window and dft => shape (freq_bins, filter_length)
57
+ # We'll make 2 conv weight tensors of shape (freq_bins, 1, filter_length).
58
+ forward_window = window_tensor.numpy() # shape (n_fft,)
59
+ forward_real = dft_real * forward_window # (freq_bins, n_fft)
60
+ forward_imag = dft_imag * forward_window
61
+
62
+ # Convert to PyTorch
63
+ forward_real_torch = torch.from_numpy(forward_real).float()
64
+ forward_imag_torch = torch.from_numpy(forward_imag).float()
65
+
66
+ # Register as Conv1d weight => (out_channels, in_channels, kernel_size)
67
+ # out_channels = freq_bins, in_channels=1, kernel_size=n_fft
68
+ self.register_buffer(
69
+ "weight_forward_real", forward_real_torch.unsqueeze(1)
70
+ )
71
+ self.register_buffer(
72
+ "weight_forward_imag", forward_imag_torch.unsqueeze(1)
73
+ )
74
+
75
+ # Precompute inverse DFT
76
+ # Real iFFT formula => scale = 1/n_fft, doubling for bins 1..freq_bins-2 if n_fft even, etc.
77
+ # For simplicity, we won't do the "DC/nyquist not doubled" approach here.
78
+ # If you want perfect real iSTFT, you can add that logic.
79
+ # This version just yields good approximate reconstruction with Hann + typical overlap.
80
+ inv_scale = 1.0 / self.n_fft
81
+ n = np.arange(self.n_fft)
82
+ angle_t = 2 * np.pi * np.outer(n, k) / self.n_fft # shape (n_fft, freq_bins)
83
+ idft_cos = np.cos(angle_t).T # => (freq_bins, n_fft)
84
+ idft_sin = np.sin(angle_t).T # => (freq_bins, n_fft)
85
+
86
+ # Multiply by window again for typical overlap-add
87
+ # We also incorporate the scale factor 1/n_fft
88
+ inv_window = window_tensor.numpy() * inv_scale
89
+ backward_real = idft_cos * inv_window # (freq_bins, n_fft)
90
+ backward_imag = idft_sin * inv_window
91
+
92
+ # We'll implement iSTFT as real+imag conv_transpose with stride=hop.
93
+ self.register_buffer(
94
+ "weight_backward_real", torch.from_numpy(backward_real).float().unsqueeze(1)
95
+ )
96
+ self.register_buffer(
97
+ "weight_backward_imag", torch.from_numpy(backward_imag).float().unsqueeze(1)
98
+ )
99
+
100
+
101
+
102
+ def transform(self, waveform: torch.Tensor):
103
+ """
104
+ Forward STFT => returns magnitude, phase
105
+ Output shape => (batch, freq_bins, frames)
106
+ """
107
+ # waveform shape => (B, T). conv1d expects (B, 1, T).
108
+ # Optional center pad
109
+ if self.center:
110
+ pad_len = self.n_fft // 2
111
+ waveform = F.pad(waveform, (pad_len, pad_len), mode=self.pad_mode)
112
+
113
+ x = waveform.unsqueeze(1) # => (B, 1, T)
114
+ # Convolution to get real part => shape (B, freq_bins, frames)
115
+ real_out = F.conv1d(
116
+ x,
117
+ self.weight_forward_real,
118
+ bias=None,
119
+ stride=self.hop_length,
120
+ padding=0,
121
+ )
122
+ # Imag part
123
+ imag_out = F.conv1d(
124
+ x,
125
+ self.weight_forward_imag,
126
+ bias=None,
127
+ stride=self.hop_length,
128
+ padding=0,
129
+ )
130
+
131
+ # magnitude, phase
132
+ magnitude = torch.sqrt(real_out**2 + imag_out**2 + 1e-14)
133
+ phase = torch.atan2(imag_out, real_out)
134
+ # Handle the case where imag_out is 0 and real_out is negative to correct ONNX atan2 to match PyTorch
135
+ # In this case, PyTorch returns pi, ONNX returns -pi
136
+ correction_mask = (imag_out == 0) & (real_out < 0)
137
+ phase[correction_mask] = torch.pi
138
+ return magnitude, phase
139
+
140
+
141
+ def inverse(self, magnitude: torch.Tensor, phase: torch.Tensor, length=None):
142
+ """
143
+ Inverse STFT => returns waveform shape (B, T).
144
+ """
145
+ # magnitude, phase => (B, freq_bins, frames)
146
+ # Re-create real/imag => shape (B, freq_bins, frames)
147
+ real_part = magnitude * torch.cos(phase)
148
+ imag_part = magnitude * torch.sin(phase)
149
+
150
+ # conv_transpose wants shape (B, freq_bins, frames). We'll treat "frames" as time dimension
151
+ # so we do (B, freq_bins, frames) => (B, freq_bins, frames)
152
+ # But PyTorch conv_transpose1d expects (B, in_channels, input_length)
153
+ real_part = real_part # (B, freq_bins, frames)
154
+ imag_part = imag_part
155
+
156
+ # real iSTFT => convolve with "backward_real", "backward_imag", and sum
157
+ # We'll do 2 conv_transpose calls, each giving (B, 1, time),
158
+ # then add them => (B, 1, time).
159
+ real_rec = F.conv_transpose1d(
160
+ real_part,
161
+ self.weight_backward_real, # shape (freq_bins, 1, filter_length)
162
+ bias=None,
163
+ stride=self.hop_length,
164
+ padding=0,
165
+ )
166
+ imag_rec = F.conv_transpose1d(
167
+ imag_part,
168
+ self.weight_backward_imag,
169
+ bias=None,
170
+ stride=self.hop_length,
171
+ padding=0,
172
+ )
173
+ # sum => (B, 1, time)
174
+ waveform = real_rec - imag_rec # typical real iFFT has minus for imaginary part
175
+
176
+ # If we used "center=True" in forward, we should remove pad
177
+ if self.center:
178
+ pad_len = self.n_fft // 2
179
+ # Because of transposed convolution, total length might have extra samples
180
+ # We remove `pad_len` from start & end if possible
181
+ waveform = waveform[..., pad_len:-pad_len]
182
+
183
+ # If a specific length is desired, clamp
184
+ if length is not None:
185
+ waveform = waveform[..., :length]
186
+
187
+ # shape => (B, T)
188
+ return waveform
189
+
190
+ def forward(self, x: torch.Tensor):
191
+ """
192
+ Full STFT -> iSTFT pass: returns time-domain reconstruction.
193
+ Same interface as your original code.
194
+ """
195
+ mag, phase = self.transform(x)
196
+ return self.inverse(mag, phase, length=x.shape[-1])
swar/kokoro/istftnet.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ADAPTED from https://github.com/yl4579/StyleTTS2/blob/main/Modules/istftnet.py
2
+ from .custom_stft import CustomSTFT
3
+ from torch.nn.utils import weight_norm
4
+ import math
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+
10
+ # https://github.com/yl4579/StyleTTS2/blob/main/Modules/utils.py
11
+ def init_weights(m, mean=0.0, std=0.01):
12
+ classname = m.__class__.__name__
13
+ if classname.find("Conv") != -1:
14
+ m.weight.data.normal_(mean, std)
15
+
16
+ def get_padding(kernel_size, dilation=1):
17
+ return int((kernel_size*dilation - dilation)/2)
18
+
19
+
20
+ class AdaIN1d(nn.Module):
21
+ def __init__(self, style_dim, num_features):
22
+ super().__init__()
23
+ # affine should be False, however there's a bug in the old torch.onnx.export (not newer dynamo) that causes the channel dimension to be lost if affine=False. When affine is true, there's additional learnably parameters. This shouldn't really matter setting it to True, since we're in inference mode
24
+ self.norm = nn.InstanceNorm1d(num_features, affine=True)
25
+ self.fc = nn.Linear(style_dim, num_features*2)
26
+
27
+ def forward(self, x, s):
28
+ h = self.fc(s)
29
+ h = h.view(h.size(0), h.size(1), 1)
30
+ gamma, beta = torch.chunk(h, chunks=2, dim=1)
31
+ return (1 + gamma) * self.norm(x) + beta
32
+
33
+
34
+ class AdaINResBlock1(nn.Module):
35
+ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5), style_dim=64):
36
+ super(AdaINResBlock1, self).__init__()
37
+ self.convs1 = nn.ModuleList([
38
+ weight_norm(nn.Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
39
+ padding=get_padding(kernel_size, dilation[0]))),
40
+ weight_norm(nn.Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
41
+ padding=get_padding(kernel_size, dilation[1]))),
42
+ weight_norm(nn.Conv1d(channels, channels, kernel_size, 1, dilation=dilation[2],
43
+ padding=get_padding(kernel_size, dilation[2])))
44
+ ])
45
+ self.convs1.apply(init_weights)
46
+ self.convs2 = nn.ModuleList([
47
+ weight_norm(nn.Conv1d(channels, channels, kernel_size, 1, dilation=1,
48
+ padding=get_padding(kernel_size, 1))),
49
+ weight_norm(nn.Conv1d(channels, channels, kernel_size, 1, dilation=1,
50
+ padding=get_padding(kernel_size, 1))),
51
+ weight_norm(nn.Conv1d(channels, channels, kernel_size, 1, dilation=1,
52
+ padding=get_padding(kernel_size, 1)))
53
+ ])
54
+ self.convs2.apply(init_weights)
55
+ self.adain1 = nn.ModuleList([
56
+ AdaIN1d(style_dim, channels),
57
+ AdaIN1d(style_dim, channels),
58
+ AdaIN1d(style_dim, channels),
59
+ ])
60
+ self.adain2 = nn.ModuleList([
61
+ AdaIN1d(style_dim, channels),
62
+ AdaIN1d(style_dim, channels),
63
+ AdaIN1d(style_dim, channels),
64
+ ])
65
+ self.alpha1 = nn.ParameterList([nn.Parameter(torch.ones(1, channels, 1)) for i in range(len(self.convs1))])
66
+ self.alpha2 = nn.ParameterList([nn.Parameter(torch.ones(1, channels, 1)) for i in range(len(self.convs2))])
67
+
68
+ def forward(self, x, s):
69
+ for c1, c2, n1, n2, a1, a2 in zip(self.convs1, self.convs2, self.adain1, self.adain2, self.alpha1, self.alpha2):
70
+ xt = n1(x, s)
71
+ xt = xt + (1 / a1) * (torch.sin(a1 * xt) ** 2) # Snake1D
72
+ xt = c1(xt)
73
+ xt = n2(xt, s)
74
+ xt = xt + (1 / a2) * (torch.sin(a2 * xt) ** 2) # Snake1D
75
+ xt = c2(xt)
76
+ x = xt + x
77
+ return x
78
+
79
+
80
+ class TorchSTFT(nn.Module):
81
+ def __init__(self, filter_length=800, hop_length=200, win_length=800, window='hann'):
82
+ super().__init__()
83
+ self.filter_length = filter_length
84
+ self.hop_length = hop_length
85
+ self.win_length = win_length
86
+ assert window == 'hann', window
87
+ self.window = torch.hann_window(win_length, periodic=True, dtype=torch.float32)
88
+
89
+ def transform(self, input_data):
90
+ forward_transform = torch.stft(
91
+ input_data,
92
+ self.filter_length, self.hop_length, self.win_length, window=self.window.to(input_data.device),
93
+ return_complex=True)
94
+ return torch.abs(forward_transform), torch.angle(forward_transform)
95
+
96
+ def inverse(self, magnitude, phase):
97
+ inverse_transform = torch.istft(
98
+ magnitude * torch.exp(phase * 1j),
99
+ self.filter_length, self.hop_length, self.win_length, window=self.window.to(magnitude.device))
100
+ return inverse_transform.unsqueeze(-2) # unsqueeze to stay consistent with conv_transpose1d implementation
101
+
102
+ def forward(self, input_data):
103
+ self.magnitude, self.phase = self.transform(input_data)
104
+ reconstruction = self.inverse(self.magnitude, self.phase)
105
+ return reconstruction
106
+
107
+
108
+ class SineGen(nn.Module):
109
+ """ Definition of sine generator
110
+ SineGen(samp_rate, harmonic_num = 0,
111
+ sine_amp = 0.1, noise_std = 0.003,
112
+ voiced_threshold = 0,
113
+ flag_for_pulse=False)
114
+ samp_rate: sampling rate in Hz
115
+ harmonic_num: number of harmonic overtones (default 0)
116
+ sine_amp: amplitude of sine-wavefrom (default 0.1)
117
+ noise_std: std of Gaussian noise (default 0.003)
118
+ voiced_thoreshold: F0 threshold for U/V classification (default 0)
119
+ flag_for_pulse: this SinGen is used inside PulseGen (default False)
120
+ Note: when flag_for_pulse is True, the first time step of a voiced
121
+ segment is always sin(torch.pi) or cos(0)
122
+ """
123
+ def __init__(self, samp_rate, upsample_scale, harmonic_num=0,
124
+ sine_amp=0.1, noise_std=0.003,
125
+ voiced_threshold=0,
126
+ flag_for_pulse=False):
127
+ super(SineGen, self).__init__()
128
+ self.sine_amp = sine_amp
129
+ self.noise_std = noise_std
130
+ self.harmonic_num = harmonic_num
131
+ self.dim = self.harmonic_num + 1
132
+ self.sampling_rate = samp_rate
133
+ self.voiced_threshold = voiced_threshold
134
+ self.flag_for_pulse = flag_for_pulse
135
+ self.upsample_scale = upsample_scale
136
+
137
+ def _f02uv(self, f0):
138
+ # generate uv signal
139
+ uv = (f0 > self.voiced_threshold).type(torch.float32)
140
+ return uv
141
+
142
+ def _f02sine(self, f0_values):
143
+ """ f0_values: (batchsize, length, dim)
144
+ where dim indicates fundamental tone and overtones
145
+ """
146
+ # convert to F0 in rad. The interger part n can be ignored
147
+ # because 2 * torch.pi * n doesn't affect phase
148
+ rad_values = (f0_values / self.sampling_rate) % 1
149
+ # initial phase noise (no noise for fundamental component)
150
+ rand_ini = torch.rand(f0_values.shape[0], f0_values.shape[2], device=f0_values.device)
151
+ rand_ini[:, 0] = 0
152
+ rad_values[:, 0, :] = rad_values[:, 0, :] + rand_ini
153
+ # instantanouse phase sine[t] = sin(2*pi \sum_i=1 ^{t} rad)
154
+ if not self.flag_for_pulse:
155
+ rad_values = F.interpolate(rad_values.transpose(1, 2), scale_factor=1/self.upsample_scale, mode="linear").transpose(1, 2)
156
+ phase = torch.cumsum(rad_values, dim=1) * 2 * torch.pi
157
+ phase = F.interpolate(phase.transpose(1, 2) * self.upsample_scale, scale_factor=self.upsample_scale, mode="linear").transpose(1, 2)
158
+ sines = torch.sin(phase)
159
+ else:
160
+ # If necessary, make sure that the first time step of every
161
+ # voiced segments is sin(pi) or cos(0)
162
+ # This is used for pulse-train generation
163
+ # identify the last time step in unvoiced segments
164
+ uv = self._f02uv(f0_values)
165
+ uv_1 = torch.roll(uv, shifts=-1, dims=1)
166
+ uv_1[:, -1, :] = 1
167
+ u_loc = (uv < 1) * (uv_1 > 0)
168
+ # get the instantanouse phase
169
+ tmp_cumsum = torch.cumsum(rad_values, dim=1)
170
+ # different batch needs to be processed differently
171
+ for idx in range(f0_values.shape[0]):
172
+ temp_sum = tmp_cumsum[idx, u_loc[idx, :, 0], :]
173
+ temp_sum[1:, :] = temp_sum[1:, :] - temp_sum[0:-1, :]
174
+ # stores the accumulation of i.phase within
175
+ # each voiced segments
176
+ tmp_cumsum[idx, :, :] = 0
177
+ tmp_cumsum[idx, u_loc[idx, :, 0], :] = temp_sum
178
+ # rad_values - tmp_cumsum: remove the accumulation of i.phase
179
+ # within the previous voiced segment.
180
+ i_phase = torch.cumsum(rad_values - tmp_cumsum, dim=1)
181
+ # get the sines
182
+ sines = torch.cos(i_phase * 2 * torch.pi)
183
+ return sines
184
+
185
+ def forward(self, f0):
186
+ """ sine_tensor, uv = forward(f0)
187
+ input F0: tensor(batchsize=1, length, dim=1)
188
+ f0 for unvoiced steps should be 0
189
+ output sine_tensor: tensor(batchsize=1, length, dim)
190
+ output uv: tensor(batchsize=1, length, 1)
191
+ """
192
+ f0_buf = torch.zeros(f0.shape[0], f0.shape[1], self.dim, device=f0.device)
193
+ # fundamental component
194
+ fn = torch.multiply(f0, torch.FloatTensor([[range(1, self.harmonic_num + 2)]]).to(f0.device))
195
+ # generate sine waveforms
196
+ sine_waves = self._f02sine(fn) * self.sine_amp
197
+ # generate uv signal
198
+ # uv = torch.ones(f0.shape)
199
+ # uv = uv * (f0 > self.voiced_threshold)
200
+ uv = self._f02uv(f0)
201
+ # noise: for unvoiced should be similar to sine_amp
202
+ # std = self.sine_amp/3 -> max value ~ self.sine_amp
203
+ # for voiced regions is self.noise_std
204
+ noise_amp = uv * self.noise_std + (1 - uv) * self.sine_amp / 3
205
+ noise = noise_amp * torch.randn_like(sine_waves)
206
+ # first: set the unvoiced part to 0 by uv
207
+ # then: additive noise
208
+ sine_waves = sine_waves * uv + noise
209
+ return sine_waves, uv, noise
210
+
211
+
212
+ class SourceModuleHnNSF(nn.Module):
213
+ """ SourceModule for hn-nsf
214
+ SourceModule(sampling_rate, harmonic_num=0, sine_amp=0.1,
215
+ add_noise_std=0.003, voiced_threshod=0)
216
+ sampling_rate: sampling_rate in Hz
217
+ harmonic_num: number of harmonic above F0 (default: 0)
218
+ sine_amp: amplitude of sine source signal (default: 0.1)
219
+ add_noise_std: std of additive Gaussian noise (default: 0.003)
220
+ note that amplitude of noise in unvoiced is decided
221
+ by sine_amp
222
+ voiced_threshold: threhold to set U/V given F0 (default: 0)
223
+ Sine_source, noise_source = SourceModuleHnNSF(F0_sampled)
224
+ F0_sampled (batchsize, length, 1)
225
+ Sine_source (batchsize, length, 1)
226
+ noise_source (batchsize, length 1)
227
+ uv (batchsize, length, 1)
228
+ """
229
+ def __init__(self, sampling_rate, upsample_scale, harmonic_num=0, sine_amp=0.1,
230
+ add_noise_std=0.003, voiced_threshod=0):
231
+ super(SourceModuleHnNSF, self).__init__()
232
+ self.sine_amp = sine_amp
233
+ self.noise_std = add_noise_std
234
+ # to produce sine waveforms
235
+ self.l_sin_gen = SineGen(sampling_rate, upsample_scale, harmonic_num,
236
+ sine_amp, add_noise_std, voiced_threshod)
237
+ # to merge source harmonics into a single excitation
238
+ self.l_linear = nn.Linear(harmonic_num + 1, 1)
239
+ self.l_tanh = nn.Tanh()
240
+
241
+ def forward(self, x):
242
+ """
243
+ Sine_source, noise_source = SourceModuleHnNSF(F0_sampled)
244
+ F0_sampled (batchsize, length, 1)
245
+ Sine_source (batchsize, length, 1)
246
+ noise_source (batchsize, length 1)
247
+ """
248
+ # source for harmonic branch
249
+ with torch.no_grad():
250
+ sine_wavs, uv, _ = self.l_sin_gen(x)
251
+ sine_merge = self.l_tanh(self.l_linear(sine_wavs))
252
+ # source for noise branch, in the same shape as uv
253
+ noise = torch.randn_like(uv) * self.sine_amp / 3
254
+ return sine_merge, noise, uv
255
+
256
+
257
+ class Generator(nn.Module):
258
+ def __init__(self, style_dim, resblock_kernel_sizes, upsample_rates, upsample_initial_channel, resblock_dilation_sizes, upsample_kernel_sizes, gen_istft_n_fft, gen_istft_hop_size, disable_complex=False):
259
+ super(Generator, self).__init__()
260
+ self.num_kernels = len(resblock_kernel_sizes)
261
+ self.num_upsamples = len(upsample_rates)
262
+ self.m_source = SourceModuleHnNSF(
263
+ sampling_rate=24000,
264
+ upsample_scale=math.prod(upsample_rates) * gen_istft_hop_size,
265
+ harmonic_num=8, voiced_threshod=10)
266
+ self.f0_upsamp = nn.Upsample(scale_factor=math.prod(upsample_rates) * gen_istft_hop_size)
267
+ self.noise_convs = nn.ModuleList()
268
+ self.noise_res = nn.ModuleList()
269
+ self.ups = nn.ModuleList()
270
+ for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
271
+ self.ups.append(weight_norm(
272
+ nn.ConvTranspose1d(upsample_initial_channel//(2**i), upsample_initial_channel//(2**(i+1)),
273
+ k, u, padding=(k-u)//2)))
274
+ self.resblocks = nn.ModuleList()
275
+ for i in range(len(self.ups)):
276
+ ch = upsample_initial_channel//(2**(i+1))
277
+ for j, (k, d) in enumerate(zip(resblock_kernel_sizes,resblock_dilation_sizes)):
278
+ self.resblocks.append(AdaINResBlock1(ch, k, d, style_dim))
279
+ c_cur = upsample_initial_channel // (2 ** (i + 1))
280
+ if i + 1 < len(upsample_rates):
281
+ stride_f0 = math.prod(upsample_rates[i + 1:])
282
+ self.noise_convs.append(nn.Conv1d(
283
+ gen_istft_n_fft + 2, c_cur, kernel_size=stride_f0 * 2, stride=stride_f0, padding=(stride_f0+1) // 2))
284
+ self.noise_res.append(AdaINResBlock1(c_cur, 7, [1,3,5], style_dim))
285
+ else:
286
+ self.noise_convs.append(nn.Conv1d(gen_istft_n_fft + 2, c_cur, kernel_size=1))
287
+ self.noise_res.append(AdaINResBlock1(c_cur, 11, [1,3,5], style_dim))
288
+ self.post_n_fft = gen_istft_n_fft
289
+ self.conv_post = weight_norm(nn.Conv1d(ch, self.post_n_fft + 2, 7, 1, padding=3))
290
+ self.ups.apply(init_weights)
291
+ self.conv_post.apply(init_weights)
292
+ self.reflection_pad = nn.ReflectionPad1d((1, 0))
293
+ self.stft = (
294
+ CustomSTFT(filter_length=gen_istft_n_fft, hop_length=gen_istft_hop_size, win_length=gen_istft_n_fft)
295
+ if disable_complex
296
+ else TorchSTFT(filter_length=gen_istft_n_fft, hop_length=gen_istft_hop_size, win_length=gen_istft_n_fft)
297
+ )
298
+
299
+ def forward(self, x, s, f0):
300
+ with torch.no_grad():
301
+ f0 = self.f0_upsamp(f0[:, None]).transpose(1, 2) # bs,n,t
302
+ har_source, noi_source, uv = self.m_source(f0)
303
+ har_source = har_source.transpose(1, 2).squeeze(1)
304
+ har_spec, har_phase = self.stft.transform(har_source)
305
+ har = torch.cat([har_spec, har_phase], dim=1)
306
+ for i in range(self.num_upsamples):
307
+ x = F.leaky_relu(x, negative_slope=0.1)
308
+ x_source = self.noise_convs[i](har)
309
+ x_source = self.noise_res[i](x_source, s)
310
+ x = self.ups[i](x)
311
+ if i == self.num_upsamples - 1:
312
+ x = self.reflection_pad(x)
313
+ x = x + x_source
314
+ xs = None
315
+ for j in range(self.num_kernels):
316
+ if xs is None:
317
+ xs = self.resblocks[i*self.num_kernels+j](x, s)
318
+ else:
319
+ xs += self.resblocks[i*self.num_kernels+j](x, s)
320
+ x = xs / self.num_kernels
321
+ x = F.leaky_relu(x)
322
+ x = self.conv_post(x)
323
+ spec = torch.exp(x[:,:self.post_n_fft // 2 + 1, :])
324
+ phase = torch.sin(x[:, self.post_n_fft // 2 + 1:, :])
325
+ return self.stft.inverse(spec, phase)
326
+
327
+
328
+ class UpSample1d(nn.Module):
329
+ def __init__(self, layer_type):
330
+ super().__init__()
331
+ self.layer_type = layer_type
332
+
333
+ def forward(self, x):
334
+ if self.layer_type == 'none':
335
+ return x
336
+ else:
337
+ return F.interpolate(x, scale_factor=2, mode='nearest')
338
+
339
+
340
+ class AdainResBlk1d(nn.Module):
341
+ def __init__(self, dim_in, dim_out, style_dim=64, actv=nn.LeakyReLU(0.2), upsample='none', dropout_p=0.0):
342
+ super().__init__()
343
+ self.actv = actv
344
+ self.upsample_type = upsample
345
+ self.upsample = UpSample1d(upsample)
346
+ self.learned_sc = dim_in != dim_out
347
+ self._build_weights(dim_in, dim_out, style_dim)
348
+ self.dropout = nn.Dropout(dropout_p)
349
+ if upsample == 'none':
350
+ self.pool = nn.Identity()
351
+ else:
352
+ self.pool = weight_norm(nn.ConvTranspose1d(dim_in, dim_in, kernel_size=3, stride=2, groups=dim_in, padding=1, output_padding=1))
353
+
354
+ def _build_weights(self, dim_in, dim_out, style_dim):
355
+ self.conv1 = weight_norm(nn.Conv1d(dim_in, dim_out, 3, 1, 1))
356
+ self.conv2 = weight_norm(nn.Conv1d(dim_out, dim_out, 3, 1, 1))
357
+ self.norm1 = AdaIN1d(style_dim, dim_in)
358
+ self.norm2 = AdaIN1d(style_dim, dim_out)
359
+ if self.learned_sc:
360
+ self.conv1x1 = weight_norm(nn.Conv1d(dim_in, dim_out, 1, 1, 0, bias=False))
361
+
362
+ def _shortcut(self, x):
363
+ x = self.upsample(x)
364
+ if self.learned_sc:
365
+ x = self.conv1x1(x)
366
+ return x
367
+
368
+ def _residual(self, x, s):
369
+ x = self.norm1(x, s)
370
+ x = self.actv(x)
371
+ x = self.pool(x)
372
+ x = self.conv1(self.dropout(x))
373
+ x = self.norm2(x, s)
374
+ x = self.actv(x)
375
+ x = self.conv2(self.dropout(x))
376
+ return x
377
+
378
+ def forward(self, x, s):
379
+ out = self._residual(x, s)
380
+ out = (out + self._shortcut(x)) * torch.rsqrt(torch.tensor(2))
381
+ return out
382
+
383
+
384
+ class Decoder(nn.Module):
385
+ def __init__(self, dim_in, style_dim, dim_out,
386
+ resblock_kernel_sizes,
387
+ upsample_rates,
388
+ upsample_initial_channel,
389
+ resblock_dilation_sizes,
390
+ upsample_kernel_sizes,
391
+ gen_istft_n_fft, gen_istft_hop_size,
392
+ disable_complex=False):
393
+ super().__init__()
394
+ self.encode = AdainResBlk1d(dim_in + 2, 1024, style_dim)
395
+ self.decode = nn.ModuleList()
396
+ self.decode.append(AdainResBlk1d(1024 + 2 + 64, 1024, style_dim))
397
+ self.decode.append(AdainResBlk1d(1024 + 2 + 64, 1024, style_dim))
398
+ self.decode.append(AdainResBlk1d(1024 + 2 + 64, 1024, style_dim))
399
+ self.decode.append(AdainResBlk1d(1024 + 2 + 64, 512, style_dim, upsample=True))
400
+ self.F0_conv = weight_norm(nn.Conv1d(1, 1, kernel_size=3, stride=2, groups=1, padding=1))
401
+ self.N_conv = weight_norm(nn.Conv1d(1, 1, kernel_size=3, stride=2, groups=1, padding=1))
402
+ self.asr_res = nn.Sequential(weight_norm(nn.Conv1d(512, 64, kernel_size=1)))
403
+ self.generator = Generator(style_dim, resblock_kernel_sizes, upsample_rates,
404
+ upsample_initial_channel, resblock_dilation_sizes,
405
+ upsample_kernel_sizes, gen_istft_n_fft, gen_istft_hop_size, disable_complex=disable_complex)
406
+
407
+ def forward(self, asr, F0_curve, N, s):
408
+ F0 = self.F0_conv(F0_curve.unsqueeze(1))
409
+ N = self.N_conv(N.unsqueeze(1))
410
+ x = torch.cat([asr, F0, N], axis=1)
411
+ x = self.encode(x, s)
412
+ asr_res = self.asr_res(asr)
413
+ res = True
414
+ for block in self.decode:
415
+ if res:
416
+ x = torch.cat([x, asr_res, F0, N], axis=1)
417
+ x = block(x, s)
418
+ if block.upsample_type != "none":
419
+ res = False
420
+ x = self.generator(x, s, F0_curve)
421
+ return x
swar/kokoro/modules.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/yl4579/StyleTTS2/blob/main/models.py
2
+ from .istftnet import AdainResBlk1d
3
+ from torch.nn.utils import weight_norm
4
+ from transformers import AlbertModel
5
+ import numpy as np
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+
10
+
11
+ class LinearNorm(nn.Module):
12
+ def __init__(self, in_dim, out_dim, bias=True, w_init_gain='linear'):
13
+ super(LinearNorm, self).__init__()
14
+ self.linear_layer = nn.Linear(in_dim, out_dim, bias=bias)
15
+ nn.init.xavier_uniform_(self.linear_layer.weight, gain=nn.init.calculate_gain(w_init_gain))
16
+
17
+ def forward(self, x):
18
+ return self.linear_layer(x)
19
+
20
+
21
+ class LayerNorm(nn.Module):
22
+ def __init__(self, channels, eps=1e-5):
23
+ super().__init__()
24
+ self.channels = channels
25
+ self.eps = eps
26
+ self.gamma = nn.Parameter(torch.ones(channels))
27
+ self.beta = nn.Parameter(torch.zeros(channels))
28
+
29
+ def forward(self, x):
30
+ x = x.transpose(1, -1)
31
+ x = F.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
32
+ return x.transpose(1, -1)
33
+
34
+
35
+ class TextEncoder(nn.Module):
36
+ def __init__(self, channels, kernel_size, depth, n_symbols, actv=nn.LeakyReLU(0.2)):
37
+ super().__init__()
38
+ self.embedding = nn.Embedding(n_symbols, channels)
39
+ padding = (kernel_size - 1) // 2
40
+ self.cnn = nn.ModuleList()
41
+ for _ in range(depth):
42
+ self.cnn.append(nn.Sequential(
43
+ weight_norm(nn.Conv1d(channels, channels, kernel_size=kernel_size, padding=padding)),
44
+ LayerNorm(channels),
45
+ actv,
46
+ nn.Dropout(0.2),
47
+ ))
48
+ self.lstm = nn.LSTM(channels, channels//2, 1, batch_first=True, bidirectional=True)
49
+
50
+ def forward(self, x, input_lengths, m):
51
+ x = self.embedding(x) # [B, T, emb]
52
+ x = x.transpose(1, 2) # [B, emb, T]
53
+ m = m.unsqueeze(1)
54
+ x.masked_fill_(m, 0.0)
55
+ for c in self.cnn:
56
+ x = c(x)
57
+ x.masked_fill_(m, 0.0)
58
+ x = x.transpose(1, 2) # [B, T, chn]
59
+ lengths = input_lengths if input_lengths.device == torch.device('cpu') else input_lengths.to('cpu')
60
+ x = nn.utils.rnn.pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False)
61
+ self.lstm.flatten_parameters()
62
+ x, _ = self.lstm(x)
63
+ x, _ = nn.utils.rnn.pad_packed_sequence(x, batch_first=True)
64
+ x = x.transpose(-1, -2)
65
+ x_pad = torch.zeros([x.shape[0], x.shape[1], m.shape[-1]], device=x.device)
66
+ x_pad[:, :, :x.shape[-1]] = x
67
+ x = x_pad
68
+ x.masked_fill_(m, 0.0)
69
+ return x
70
+
71
+
72
+ class AdaLayerNorm(nn.Module):
73
+ def __init__(self, style_dim, channels, eps=1e-5):
74
+ super().__init__()
75
+ self.channels = channels
76
+ self.eps = eps
77
+ self.fc = nn.Linear(style_dim, channels*2)
78
+
79
+ def forward(self, x, s):
80
+ x = x.transpose(-1, -2)
81
+ x = x.transpose(1, -1)
82
+ h = self.fc(s)
83
+ h = h.view(h.size(0), h.size(1), 1)
84
+ gamma, beta = torch.chunk(h, chunks=2, dim=1)
85
+ gamma, beta = gamma.transpose(1, -1), beta.transpose(1, -1)
86
+ x = F.layer_norm(x, (self.channels,), eps=self.eps)
87
+ x = (1 + gamma) * x + beta
88
+ return x.transpose(1, -1).transpose(-1, -2)
89
+
90
+
91
+ class ProsodyPredictor(nn.Module):
92
+ def __init__(self, style_dim, d_hid, nlayers, max_dur=50, dropout=0.1):
93
+ super().__init__()
94
+ self.text_encoder = DurationEncoder(sty_dim=style_dim, d_model=d_hid,nlayers=nlayers, dropout=dropout)
95
+ self.lstm = nn.LSTM(d_hid + style_dim, d_hid // 2, 1, batch_first=True, bidirectional=True)
96
+ self.duration_proj = LinearNorm(d_hid, max_dur)
97
+ self.shared = nn.LSTM(d_hid + style_dim, d_hid // 2, 1, batch_first=True, bidirectional=True)
98
+ self.F0 = nn.ModuleList()
99
+ self.F0.append(AdainResBlk1d(d_hid, d_hid, style_dim, dropout_p=dropout))
100
+ self.F0.append(AdainResBlk1d(d_hid, d_hid // 2, style_dim, upsample=True, dropout_p=dropout))
101
+ self.F0.append(AdainResBlk1d(d_hid // 2, d_hid // 2, style_dim, dropout_p=dropout))
102
+ self.N = nn.ModuleList()
103
+ self.N.append(AdainResBlk1d(d_hid, d_hid, style_dim, dropout_p=dropout))
104
+ self.N.append(AdainResBlk1d(d_hid, d_hid // 2, style_dim, upsample=True, dropout_p=dropout))
105
+ self.N.append(AdainResBlk1d(d_hid // 2, d_hid // 2, style_dim, dropout_p=dropout))
106
+ self.F0_proj = nn.Conv1d(d_hid // 2, 1, 1, 1, 0)
107
+ self.N_proj = nn.Conv1d(d_hid // 2, 1, 1, 1, 0)
108
+
109
+ def forward(self, texts, style, text_lengths, alignment, m):
110
+ d = self.text_encoder(texts, style, text_lengths, m)
111
+ m = m.unsqueeze(1)
112
+ lengths = text_lengths if text_lengths.device == torch.device('cpu') else text_lengths.to('cpu')
113
+ x = nn.utils.rnn.pack_padded_sequence(d, lengths, batch_first=True, enforce_sorted=False)
114
+ self.lstm.flatten_parameters()
115
+ x, _ = self.lstm(x)
116
+ x, _ = nn.utils.rnn.pad_packed_sequence(x, batch_first=True)
117
+ x_pad = torch.zeros([x.shape[0], m.shape[-1], x.shape[-1]], device=x.device)
118
+ x_pad[:, :x.shape[1], :] = x
119
+ x = x_pad
120
+ duration = self.duration_proj(nn.functional.dropout(x, 0.5, training=False))
121
+ en = (d.transpose(-1, -2) @ alignment)
122
+ return duration.squeeze(-1), en
123
+
124
+ def F0Ntrain(self, x, s):
125
+ x, _ = self.shared(x.transpose(-1, -2))
126
+ F0 = x.transpose(-1, -2)
127
+ for block in self.F0:
128
+ F0 = block(F0, s)
129
+ F0 = self.F0_proj(F0)
130
+ N = x.transpose(-1, -2)
131
+ for block in self.N:
132
+ N = block(N, s)
133
+ N = self.N_proj(N)
134
+ return F0.squeeze(1), N.squeeze(1)
135
+
136
+
137
+ class DurationEncoder(nn.Module):
138
+ def __init__(self, sty_dim, d_model, nlayers, dropout=0.1):
139
+ super().__init__()
140
+ self.lstms = nn.ModuleList()
141
+ for _ in range(nlayers):
142
+ self.lstms.append(nn.LSTM(d_model + sty_dim, d_model // 2, num_layers=1, batch_first=True, bidirectional=True, dropout=dropout))
143
+ self.lstms.append(AdaLayerNorm(sty_dim, d_model))
144
+ self.dropout = dropout
145
+ self.d_model = d_model
146
+ self.sty_dim = sty_dim
147
+
148
+ def forward(self, x, style, text_lengths, m):
149
+ masks = m
150
+ x = x.permute(2, 0, 1)
151
+ s = style.expand(x.shape[0], x.shape[1], -1)
152
+ x = torch.cat([x, s], axis=-1)
153
+ x.masked_fill_(masks.unsqueeze(-1).transpose(0, 1), 0.0)
154
+ x = x.transpose(0, 1)
155
+ x = x.transpose(-1, -2)
156
+ for block in self.lstms:
157
+ if isinstance(block, AdaLayerNorm):
158
+ x = block(x.transpose(-1, -2), style).transpose(-1, -2)
159
+ x = torch.cat([x, s.permute(1, 2, 0)], axis=1)
160
+ x.masked_fill_(masks.unsqueeze(-1).transpose(-1, -2), 0.0)
161
+ else:
162
+ lengths = text_lengths if text_lengths.device == torch.device('cpu') else text_lengths.to('cpu')
163
+ x = x.transpose(-1, -2)
164
+ x = nn.utils.rnn.pack_padded_sequence(
165
+ x, lengths, batch_first=True, enforce_sorted=False)
166
+ block.flatten_parameters()
167
+ x, _ = block(x)
168
+ x, _ = nn.utils.rnn.pad_packed_sequence(
169
+ x, batch_first=True)
170
+ x = F.dropout(x, p=self.dropout, training=False)
171
+ x = x.transpose(-1, -2)
172
+ x_pad = torch.zeros([x.shape[0], x.shape[1], m.shape[-1]], device=x.device)
173
+ x_pad[:, :, :x.shape[-1]] = x
174
+ x = x_pad
175
+
176
+ return x.transpose(-1, -2)
177
+
178
+
179
+ # https://github.com/yl4579/StyleTTS2/blob/main/Utils/PLBERT/util.py
180
+ class CustomAlbert(AlbertModel):
181
+ def forward(self, *args, **kwargs):
182
+ outputs = super().forward(*args, **kwargs)
183
+ return outputs.last_hidden_state
swar/mel.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """16 kHz log-mel, exactly as Swar's style encoders were trained on it."""
2
+ from __future__ import annotations
3
+
4
+ import torch
5
+ import torchaudio
6
+ from torch import nn
7
+
8
+ SR, N_FFT, WIN, HOP, N_MELS = 16000, 1024, 800, 200, 80
9
+ MEAN, STD = -4.0, 4.0
10
+
11
+
12
+ class Mel(nn.Module):
13
+ def __init__(self):
14
+ super().__init__()
15
+ self.mel = torchaudio.transforms.MelSpectrogram(
16
+ sample_rate=SR, n_fft=N_FFT, win_length=WIN, hop_length=HOP, n_mels=N_MELS,
17
+ f_min=0.0, f_max=8000.0, center=True, pad_mode="reflect", power=2.0)
18
+
19
+ def forward(self, wav: torch.Tensor) -> torch.Tensor:
20
+ if wav.dtype == torch.int16:
21
+ wav = wav.float() / 32768.0
22
+ if wav.dim() == 1:
23
+ wav = wav[None]
24
+ return (torch.log(1e-5 + self.mel(wav)) - MEAN) / STD
25
+
26
+ @staticmethod
27
+ def frames(n_samples: int) -> int:
28
+ return n_samples // HOP + 1
swar/model.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """The Swar network, built from Kokoro's own layer classes (swar/kokoro, Apache-2.0).
3
+
4
+ from swar.model import build
5
+ m = build("D", config) # dict of nn.Modules, widths from config.json
6
+
7
+ Stage C trains text_encoder + decoder + style_encoder (acoustic); stage D adds
8
+ plbert + bert_encoder + predictor + predictor_encoder (prosodic style). At
9
+ inference the two 128-d styles concatenate into Kokoro's 256-d ref_s layout:
10
+ [:128] drives the decoder (timbre), [128:] drives the prosody predictor.
11
+
12
+ Two deliberate departures from the Kokoro code, both because we run at 16 kHz:
13
+ * Kokoro's Generator hard-codes SourceModuleHnNSF(sampling_rate=24000). Its sine
14
+ source turns F0 into a waveform at that rate, so at 16 kHz every voice would
15
+ come out 1.5x too high. The source is rebuilt at our rate.
16
+ * Kokoro's Decoder hard-codes its widths (1024 / 512 / 64); they come from
17
+ config.json here (512 / 256 / 64).
18
+ The style encoder is StyleTTS2's (Kokoro ships none -- it ships voice packs).
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import math
24
+ from pathlib import Path
25
+
26
+ import torch
27
+ import torch.nn.functional as F
28
+ from torch import nn
29
+ from torch.nn.utils import spectral_norm, weight_norm
30
+
31
+ from .kokoro.istftnet import AdainResBlk1d, Generator, SourceModuleHnNSF
32
+ from .kokoro.modules import ProsodyPredictor, TextEncoder
33
+
34
+ SR = 16000
35
+
36
+
37
+ def conf(path: str | Path | None = None) -> dict:
38
+ p = Path(path) if path else Path(__file__).resolve().parents[1] / "config.json"
39
+ return json.loads(p.read_text())
40
+
41
+
42
+ class Decoder(nn.Module):
43
+ """Kokoro's istftnet.Decoder: aligned text features + F0 + energy + style -> waveform."""
44
+
45
+ def __init__(self, dim_in, style_dim, hid, out, res, gen: dict, sr: int = SR):
46
+ super().__init__()
47
+ self.encode = AdainResBlk1d(dim_in + 2, hid, style_dim)
48
+ self.decode = nn.ModuleList([
49
+ AdainResBlk1d(hid + 2 + res, hid, style_dim),
50
+ AdainResBlk1d(hid + 2 + res, hid, style_dim),
51
+ AdainResBlk1d(hid + 2 + res, hid, style_dim),
52
+ AdainResBlk1d(hid + 2 + res, out, style_dim, upsample=True),
53
+ ])
54
+ self.F0_conv = weight_norm(nn.Conv1d(1, 1, kernel_size=3, stride=2, groups=1, padding=1))
55
+ self.N_conv = weight_norm(nn.Conv1d(1, 1, kernel_size=3, stride=2, groups=1, padding=1))
56
+ self.asr_res = nn.Sequential(weight_norm(nn.Conv1d(dim_in, res, kernel_size=1)))
57
+ self.generator = Generator(style_dim, **gen)
58
+ self.generator.m_source = SourceModuleHnNSF(
59
+ sampling_rate=sr,
60
+ upsample_scale=math.prod(gen["upsample_rates"]) * gen["gen_istft_hop_size"],
61
+ harmonic_num=8, voiced_threshod=10)
62
+
63
+ def forward(self, asr, F0_curve, N, s):
64
+ """asr [B, C, T] (40 Hz), F0/N [B, 2T] (80 Hz), s [B, style] -> [B, 1, 400 T]."""
65
+ F0 = self.F0_conv(F0_curve.unsqueeze(1))
66
+ N = self.N_conv(N.unsqueeze(1))
67
+ x = torch.cat([asr, F0, N], axis=1)
68
+ x = self.encode(x, s)
69
+ asr_res = self.asr_res(asr)
70
+ res = True
71
+ for block in self.decode:
72
+ if res:
73
+ x = torch.cat([x, asr_res, F0, N], axis=1)
74
+ x = block(x, s)
75
+ if block.upsample_type != "none":
76
+ res = False
77
+ return self.generator(x, s, F0_curve)
78
+
79
+
80
+ # ---- StyleTTS2 style encoder (Modules from yl4579/StyleTTS2 models.py, MIT) --------
81
+ class LearnedDownSample(nn.Module):
82
+ def __init__(self, dim_in):
83
+ super().__init__()
84
+ self.conv = spectral_norm(nn.Conv2d(dim_in, dim_in, kernel_size=(3, 3), stride=(2, 2),
85
+ groups=dim_in, padding=1))
86
+
87
+ def forward(self, x):
88
+ return self.conv(x)
89
+
90
+
91
+ class ResBlk(nn.Module):
92
+ def __init__(self, dim_in, dim_out):
93
+ super().__init__()
94
+ self.actv = nn.LeakyReLU(0.2)
95
+ self.learned_sc = dim_in != dim_out
96
+ self.conv1 = spectral_norm(nn.Conv2d(dim_in, dim_in, 3, 1, 1))
97
+ self.conv2 = spectral_norm(nn.Conv2d(dim_in, dim_out, 3, 1, 1))
98
+ self.down_res = LearnedDownSample(dim_in)
99
+ if self.learned_sc:
100
+ self.conv1x1 = spectral_norm(nn.Conv2d(dim_in, dim_out, 1, 1, 0, bias=False))
101
+
102
+ def _shortcut(self, x):
103
+ if self.learned_sc:
104
+ x = self.conv1x1(x)
105
+ return F.avg_pool2d(x, 2)
106
+
107
+ def _residual(self, x):
108
+ x = self.conv1(self.actv(x))
109
+ x = self.down_res(x)
110
+ return self.conv2(self.actv(x))
111
+
112
+ def forward(self, x):
113
+ return (self._shortcut(x) + self._residual(x)) / math.sqrt(2)
114
+
115
+
116
+ class StyleEncoder(nn.Module):
117
+ """mel [B, 1, 80, T] -> [B, style_dim]. T >= 80 frames (1 s) recommended."""
118
+
119
+ def __init__(self, dim_in=32, style_dim=128, max_conv_dim=256):
120
+ super().__init__()
121
+ blocks = [spectral_norm(nn.Conv2d(1, dim_in, 3, 1, 1))]
122
+ d = dim_in
123
+ for _ in range(4):
124
+ o = min(d * 2, max_conv_dim)
125
+ blocks.append(ResBlk(d, o))
126
+ d = o
127
+ blocks += [nn.LeakyReLU(0.2), spectral_norm(nn.Conv2d(d, d, 5, 1, 0)),
128
+ nn.AdaptiveAvgPool2d(1), nn.LeakyReLU(0.2)]
129
+ self.shared = nn.Sequential(*blocks)
130
+ self.unshared = nn.Linear(d, style_dim)
131
+
132
+ def forward(self, x):
133
+ # 4 halvings: time must be a multiple of 16 or the pooled shortcut and the
134
+ # strided residual disagree by a frame (2 s of audio = 161 mel frames).
135
+ x = x[..., : (x.shape[-1] // 16) * 16]
136
+ return self.unshared(self.shared(x).flatten(1))
137
+
138
+
139
+ def build(stage: str = "C", c: dict | None = None) -> nn.ModuleDict:
140
+ c = c or conf()
141
+ se = c["style_encoder"]
142
+ m = {
143
+ "text_encoder": TextEncoder(c["hidden_dim"], c["text_encoder_kernel_size"], c["n_layer"],
144
+ c["n_token"]),
145
+ "decoder": Decoder(c["hidden_dim"], c["style_dim"], c["decoder"]["hid"], c["decoder"]["out"],
146
+ c["decoder"]["res"], c["istftnet"], sr=c["sample_rate"]),
147
+ "style_encoder": StyleEncoder(se["dim_in"], c["style_dim"], se["max_conv_dim"]),
148
+ }
149
+ if stage != "C":
150
+ from transformers import AlbertConfig, AlbertModel
151
+ m["plbert"] = AlbertModel(AlbertConfig(vocab_size=c["n_token"], hidden_act="gelu", **c["plbert"]),
152
+ add_pooling_layer=False)
153
+ m["bert_encoder"] = nn.Linear(c["plbert"]["hidden_size"], c["hidden_dim"])
154
+ m["predictor"] = ProsodyPredictor(c["style_dim"], c["hidden_dim"], c["n_layer"], c["max_dur"],
155
+ c["dropout"])
156
+ m["predictor_encoder"] = StyleEncoder(se["dim_in"], c["style_dim"], se["max_conv_dim"])
157
+ return nn.ModuleDict(m)
158
+
159
+
160
+ def energy(mel_norm: torch.Tensor) -> torch.Tensor:
161
+ """StyleTTS2's N target: log of the L2 norm of the LINEAR mel frame, [B, 80, T] -> [B, T]."""
162
+ from .mel import MEAN, STD
163
+ return torch.log(torch.exp(mel_norm * STD + MEAN).norm(dim=1) + 1e-5)
164
+
165
+
swar/text.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """The text front-end of Swar: raw text -> the letters the model reads.
3
+
4
+ from swar.text import normalize, to_ids, VOCAB
5
+ normalize("तपाईंको balance Rs. 250 छ।") -> "तपाईंको balance दुई सय पचास rupees छ."
6
+
7
+ RULES, all enforced here so training and inference cannot drift:
8
+ * Output holds ONLY Devanagari letters/signs, Latin letters, space, apostrophe and
9
+ ordinary punctuation. No tags, no markers, no language switches -- the script
10
+ a letter is written in IS its language.
11
+ * Numbers become words in the language of the text around them (nearest letter
12
+ wins): Devanagari context -> Nepali words, Latin context -> English words.
13
+ Digit runs of 6+ are identifiers (phone, account, OTP) and are read digit by
14
+ digit with "double" (डबल in Nepali).
15
+ * Anything else is dropped (normalize) or makes the sentence unusable
16
+ (is_clean), so a stray symbol can never reach the model as an unknown id.
17
+
18
+ This file is the vocabulary too: VOCAB is fixed here, and ids are positions in it,
19
+ so a checkpoint is only valid with the VOCAB it was trained on (saved inside it).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ import unicodedata
25
+
26
+ # ---- vocabulary ----------------------------------------------------------------
27
+ PAD, BOS, EOS = "<pad>", "<s>", "</s>" # internal ids, never in text
28
+ PUNCT = list(".,?!;:'\"-()…—")
29
+ LATIN = [chr(c) for c in range(ord("a"), ord("z") + 1)] + \
30
+ [chr(c) for c in range(ord("A"), ord("Z") + 1)]
31
+ # Every Devanagari code point Nepali text uses: U+0900..U+0963 (letters, signs,
32
+ # vowel signs, virama, nukta) plus U+0970..U+097F; digits U+0966..U+096F are
33
+ # excluded because numbers are always verbalised first; dandas fold to "."
34
+ DEVANAGARI = [chr(c) for c in range(0x0900, 0x0964)] + [chr(c) for c in range(0x0970, 0x0980)]
35
+ MASK = "<mask>" # stage A (PL-BERT) only; appended LAST so
36
+ VOCAB = [PAD, BOS, EOS, " "] + PUNCT + LATIN + DEVANAGARI + [MASK] # no other id moves
37
+ assert len(VOCAB) <= 256, len(VOCAB)
38
+ STOI = {s: i for i, s in enumerate(VOCAB)}
39
+ ALLOWED = set(VOCAB[3:-1])
40
+
41
+ # ---- character folding -----------------------------------------------------------
42
+ _FOLD = {
43
+ "।": ".", "॥": ".", "’": "'", "‘": "'", "`": "'", "“": '"', "”": '"',
44
+ "–": "-", "‐": "-", "‑": "-", "...": "…", " ": " ",
45
+ }
46
+ _DROP = dict.fromkeys(map(ord, "​‌‍‎‏"), None)
47
+
48
+ # ---- English numbers ---------------------------------------------------------------
49
+ _ONES = ("zero one two three four five six seven eight nine ten eleven twelve thirteen "
50
+ "fourteen fifteen sixteen seventeen eighteen nineteen").split()
51
+ _TENS = "_ _ twenty thirty forty fifty sixty seventy eighty ninety".split()
52
+ _SCALES = [(10 ** 9, "billion"), (10 ** 6, "million"), (1000, "thousand")]
53
+ _ORD = {"one": "first", "two": "second", "three": "third", "five": "fifth",
54
+ "eight": "eighth", "nine": "ninth", "twelve": "twelfth"}
55
+
56
+
57
+ def en_cardinal(n: int) -> str:
58
+ if n < 20:
59
+ return _ONES[n]
60
+ if n < 100:
61
+ return _TENS[n // 10] + ("" if n % 10 == 0 else "-" + _ONES[n % 10])
62
+ if n < 1000:
63
+ rest = n % 100
64
+ return _ONES[n // 100] + " hundred" + ("" if rest == 0 else " and " + en_cardinal(rest))
65
+ for v, w in _SCALES:
66
+ if n >= v:
67
+ rest = n % v
68
+ head = en_cardinal(n // v) + " " + w
69
+ if rest == 0:
70
+ return head
71
+ return head + (" and " if rest < 100 else " ") + en_cardinal(rest)
72
+ raise ValueError(n)
73
+
74
+
75
+ def en_ordinal(n: int) -> str:
76
+ w = en_cardinal(n)
77
+ last = re.split(r"([ -])", w)
78
+ tail = last[-1]
79
+ if tail in _ORD:
80
+ tail = _ORD[tail]
81
+ elif tail.endswith("y"):
82
+ tail = tail[:-1] + "ieth"
83
+ else:
84
+ tail += "th"
85
+ return "".join(last[:-1]) + tail
86
+
87
+
88
+ def en_digits(run: str) -> str:
89
+ """Identifier reading: 9851005996 -> nine eight five one double zero ..."""
90
+ out, i = [], 0
91
+ while i < len(run):
92
+ if i + 1 < len(run) and run[i] == run[i + 1]:
93
+ out.append("double " + _ONES[int(run[i])])
94
+ i += 2
95
+ else:
96
+ out.append(_ONES[int(run[i])])
97
+ i += 1
98
+ return " ".join(out)
99
+
100
+
101
+ # ---- Nepali numbers ------------------------------------------------------------------
102
+ _NE_ONES = ("शून्य एक दुई तीन चार पाँच छ सात आठ नौ दश एघार बाह्र तेह्र चौध पन्ध्र सोह्र सत्र "
103
+ "अठार उन्नाइस बीस एक्काइस बाइस तेइस चौबीस पच्चीस छब्बीस सत्ताइस अठ्ठाइस उनन्तीस "
104
+ "तीस एकतीस बत्तीस तेत्तीस चौँतीस पैँतीस छत्तीस सैँतीस अठतीस उनन्च���लीस चालीस "
105
+ "एकचालीस बयालीस त्रिचालीस चवालीस पैँतालीस छयालीस सच्चालीस अठचालीस उनन्चास पचास "
106
+ "एकाउन्न बाउन्न त्रिपन्न चवन्न पचपन्न छपन्न सन्ताउन्न अन्ठाउन्न उनन्साठी साठी "
107
+ "एकसट्ठी बयसट्ठी त्रिसट्ठी चौसट्ठी पैँसट्ठी छयसट्ठी सतसट्ठी अठसट्ठी उनन्सत्तरी सत्तरी "
108
+ "एकहत्तर बहत्तर त्रिहत्तर चौहत्तर पचहत्तर छयहत्तर सतहत्तर अठहत्तर उनासी असी "
109
+ "एकासी बयासी त्रियासी चौरासी पचासी छयासी सतासी अठासी उनान्नब्बे नब्बे "
110
+ "एकानब्बे बयानब्बे त्रियानब्बे चौरानब्बे पन्चानब्बे छयानब्बे सन्तानब्बे अन्ठानब्बे "
111
+ "उनान्सय").split()
112
+ assert len(_NE_ONES) == 100, len(_NE_ONES)
113
+ _NE_SCALES = [(10 ** 7, "करोड"), (10 ** 5, "लाख"), (1000, "हजार"), (100, "सय")]
114
+
115
+
116
+ def ne_cardinal(n: int) -> str:
117
+ if n < 100:
118
+ return _NE_ONES[n]
119
+ for v, w in _NE_SCALES:
120
+ if n >= v:
121
+ rest = n % v
122
+ head = ne_cardinal(n // v) + " " + w
123
+ return head if rest == 0 else head + " " + ne_cardinal(rest)
124
+ raise ValueError(n)
125
+
126
+
127
+ _NE_DIGIT = dict(zip("0123456789", _NE_ONES[:10]))
128
+
129
+
130
+ def ne_digits(run: str) -> str:
131
+ """'9851005996' -> 'नौ आठ पाँच एक डबल शून्य पाँच डबल नौ छ'"""
132
+ d = run.translate(str.maketrans("०१२३४५६७८९", "0123456789"))
133
+ words, i = [], 0
134
+ while i < len(d):
135
+ if i + 1 < len(d) and d[i] == d[i + 1]:
136
+ words.append("डबल " + _NE_DIGIT[d[i]])
137
+ i += 2
138
+ else:
139
+ words.append(_NE_DIGIT[d[i]])
140
+ i += 1
141
+ return " ".join(words)
142
+
143
+
144
+ # ---- script context -------------------------------------------------------------------
145
+ def _is_dev(ch: str) -> bool:
146
+ return 0x0900 <= ord(ch) <= 0x097F and not ("०" <= ch <= "९")
147
+
148
+
149
+ def _context_is_nepali(s: str, start: int, end: int) -> bool:
150
+ """The sentence's majority script decides; the nearest letter only breaks ties.
151
+
152
+ "तपाईंको balance Rs. 250 छ" is a Nepali sentence with one English loanword,
153
+ and a Nepali speaker reads its number in Nepali -- nearest-letter alone would
154
+ say English, because "balance" is closer than "छ".
155
+ """
156
+ dev = sum(1 for ch in s if _is_dev(ch))
157
+ lat = sum(1 for ch in s if ch.isascii() and ch.isalpha())
158
+ if dev != lat:
159
+ return dev > lat
160
+ for d in range(1, max(start, len(s) - end) + 1):
161
+ for j in (start - d, end - 1 + d):
162
+ if 0 <= j < len(s) and s[j].isalpha():
163
+ return _is_dev(s[j])
164
+ return False
165
+
166
+
167
+ _CURRENCY = re.compile(r"(?:\b(?:Rs|NPR|NRs)\.?|रु\.?|\$)\s*(?=[0-9०-९])")
168
+ _NUM = re.compile(r"[0-9०-९][0-9०-९,]*(?:\.[0-9०-९]+)*(?:st|nd|rd|th)?")
169
+ _TIME = re.compile(r"(?<![0-9०-९:])([0-9०-९]{1,2}):([0-9०-९]{2})(?![0-9०-९:])")
170
+ _DEV2ASCII = str.maketrans("०१२३४५६७८९", "0123456789")
171
+
172
+
173
+ def _verbalise_numbers(s: str) -> str:
174
+ # currency prefix -> spoken unit after the number ("Rs 250" -> "250 rupees")
175
+ # Placeholders are control characters, NOT letters: a placeholder made of
176
+ # letters would itself be counted by the script-context check.
177
+ def cur(m):
178
+ return "\x01" if not m.group(0).startswith("$") else "\x02"
179
+ s = _CURRENCY.sub(cur, s)
180
+ s = _TIME.sub(lambda m: _time(s, m), s)
181
+
182
+ out, last = [], 0
183
+ for m in _NUM.finditer(s):
184
+ try:
185
+ word_or_none = _one_number(s, m, last, out)
186
+ except (ValueError, IndexError):
187
+ word_or_none = None
188
+ if word_or_none is None:
189
+ # Unreadable ("1,4th", "3.2.1"): leave the digits. They are not in
190
+ # VOCAB, so is_clean() rejects the sentence instead of the model
191
+ # hearing a made-up reading.
192
+ out.append(s[last:m.end()])
193
+ last = m.end()
194
+ out.append(s[last:])
195
+ return "".join(out).replace("\x01", "").replace("\x02", "")
196
+
197
+
198
+ def _time(s, m):
199
+ """5:30 -> 'five thirty' / 'पाँच बजेर तीस मिनेट'; 9:00 -> "nine o'clock" / 'नौ बजे'."""
200
+ h, mi = (int(x.translate(_DEV2ASCII)) for x in m.groups())
201
+ if h > 24 or mi > 59:
202
+ return m.group(0)
203
+ if _context_is_nepali(s, m.start(), m.end()):
204
+ return ne_cardinal(h) + " बजे" + ("" if mi == 0 else "र " + ne_cardinal(mi) + " मिनेट")
205
+ if mi == 0:
206
+ return en_cardinal(h) + " o'clock"
207
+ return en_cardinal(h) + " " + ("oh " + _ONES[mi] if mi < 10 else en_cardinal(mi))
208
+
209
+
210
+ def _one_number(s, m, last, out):
211
+ """Append the spoken form of number match m to out; return True, or None."""
212
+ if m.group(0).translate(_DEV2ASCII).count(".") > 1 or re.match(r"[.:/][0-9०-९]", s[m.end():m.end() + 2]):
213
+ return None # 3.2.1, 1/2, 12:30:15 -- no safe reading
214
+ tok = m.group(0)
215
+ ne = _context_is_nepali(s, m.start(), m.end())
216
+ raw = tok.translate(_DEV2ASCII)
217
+ suffix = ""
218
+ mo = re.match(r"^(.*?)(st|nd|rd|th)$", raw)
219
+ if mo and mo.group(1).isdigit():
220
+ raw, suffix = mo.group(1), "ord"
221
+ digits = raw.replace(",", "")
222
+ pre = s[last:m.start()]
223
+ money = pre.endswith(("\x01", "\x02"))
224
+ if money and "." in digits and len(digits.split(".", 1)[1]) == 2:
225
+ a, b = (int(x) for x in digits.split(".", 1))
226
+ usd = pre.endswith("\x02")
227
+ if ne:
228
+ big, small = ("डलर", "सेन्ट") if usd else ("रुपैयाँ", "पैसा")
229
+ word = f"{ne_cardinal(a)} {big}" + (f" {ne_cardinal(b)} {small}" if b else "")
230
+ else:
231
+ big, small = ("dollars", "cents") if usd else ("rupees", "paisa")
232
+ word = f"{en_cardinal(a)} {big}" + (f" and {en_cardinal(b)} {small}" if b else "")
233
+ out.append(pre[:-1] + word)
234
+ return True
235
+ if "." in digits:
236
+ a, b = digits.split(".", 1)
237
+ word = ((ne_cardinal(int(a)) + " दशमलव " + " ".join(_NE_ONES[int(c)] for c in b))
238
+ if ne else
239
+ (en_cardinal(int(a)) + " point " + " ".join(_ONES[int(c)] for c in b)))
240
+ elif len(digits) >= 6 or (digits.startswith("0") and len(digits) > 1):
241
+ word = ne_digits(digits) if ne else en_digits(digits)
242
+ elif suffix == "ord" and not ne:
243
+ word = en_ordinal(int(digits))
244
+ else:
245
+ word = ne_cardinal(int(digits)) if ne else en_cardinal(int(digits))
246
+ unit = ""
247
+ if pre.endswith("\x01"):
248
+ pre, unit = pre[:-1], (" रुपैयाँ" if ne else " rupees")
249
+ elif pre.endswith("\x02"):
250
+ pre, unit = pre[:-1], (" डलर" if ne else " dollars")
251
+ out.append(pre + word + unit)
252
+ return True
253
+
254
+
255
+ # ---- public API -------------------------------------------------------------------------
256
+ def normalize(text: str) -> str:
257
+ """Raw text -> model letters. Same function for training data and live requests."""
258
+ s = unicodedata.normalize("NFC", text or "").translate(_DROP)
259
+ for a, b in _FOLD.items():
260
+ s = s.replace(a, b)
261
+ s = _verbalise_numbers(s)
262
+ s = s.replace("%", " percent").replace("&", " and ")
263
+ s = "".join(ch if ch in ALLOWED else (" " if ch.isspace() else "") for ch in s)
264
+ s = re.sub(r"\s+", " ", s).strip()
265
+ s = re.sub(r"\s+([.,?!;:…])", r"\1", s) # no space before punctuation
266
+ s = re.sub(r"([.?!])\1+", r"\1", s)
267
+ return s
268
+
269
+
270
+ def is_clean(raw: str, normalized: str) -> bool:
271
+ """True if normalising dropped nothing but whitespace and known foldings --
272
+ i.e. the sentence can be trained on without its text silently differing from
273
+ what the audio says."""
274
+ s = unicodedata.normalize("NFC", raw or "").translate(_DROP)
275
+ for a, b in _FOLD.items():
276
+ s = s.replace(a, b)
277
+ s = _verbalise_numbers(s).replace("%", " percent").replace("&", " and ")
278
+ return all(ch in ALLOWED or ch.isspace() for ch in s) and bool(normalized)
279
+
280
+
281
+ def to_ids(normalized: str) -> list[int]:
282
+ return [STOI[BOS]] + [STOI[c] for c in normalized] + [STOI[EOS]]
283
+
284
+
285
+ if __name__ == "__main__":
286
+ print(f"VOCAB {len(VOCAB)} symbols: {len(PUNCT)} punct, {len(LATIN)} Latin, "
287
+ f"{len(DEVANAGARI)} Devanagari, 4 internal/space")
288
+ for t in ["तपाईंको balance Rs. 250 छ।",
289
+ "Your balance is Rs 1,250.50, valid until the 15th.",
290
+ "Please call 9851005996 or 015574574.",
291
+ "मेरो नम्बर ९८५१००५९९६ हो, र म ५ बजे आउँछु।",
292
+ "रु ५०० को रिचार्ज गर्नुहोस्",
293
+ "A & B give 20% off… ‘really’?",
294
+ "emoji 😀 and © symbols are dropped"]:
295
+ n = normalize(t)
296
+ print(f"{'clean' if is_clean(t, n) else 'DIRTY'} {t!r}\n -> {n!r}")
voices/bijaya.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e58fb5a796bcb6ca55c6a5e3740035bd8fb415916a0098bcc84aa59be002580d
3
+ size 2450