Swar 0.1.0: stage E release with default voice bijaya
Browse files- LICENSE +6 -0
- README.md +152 -0
- config.json +64 -0
- requirements.txt +7 -0
- swar.safetensors +3 -0
- swar/__init__.py +5 -0
- swar/inference.py +175 -0
- swar/kokoro/LICENSE +201 -0
- swar/kokoro/__init__.py +2 -0
- swar/kokoro/custom_stft.py +196 -0
- swar/kokoro/istftnet.py +421 -0
- swar/kokoro/modules.py +183 -0
- swar/mel.py +28 -0
- swar/model.py +165 -0
- swar/text.py +296 -0
- voices/bijaya.pt +3 -0
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
|