Text-to-Speech
ONNX
GGUF
Chinese
English
onnxruntime
tts
on-device
jetson
telephony
vits
mb-istft-vits
multi-speaker
mandarin
taiwanese-mandarin
imatrix
conversational
Instructions to use Luigi/PrimeTTS with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use Luigi/PrimeTTS with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="Luigi/PrimeTTS", filename="streaming_llm/gemma270m_it_q8.gguf", )
llm.create_chat_completion( messages = "\"The answer to the universe is 42\"" )
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use Luigi/PrimeTTS with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf Luigi/PrimeTTS:F32 # Run inference directly in the terminal: llama cli -hf Luigi/PrimeTTS:F32
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf Luigi/PrimeTTS:F32 # Run inference directly in the terminal: llama cli -hf Luigi/PrimeTTS:F32
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf Luigi/PrimeTTS:F32 # Run inference directly in the terminal: ./llama-cli -hf Luigi/PrimeTTS:F32
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf Luigi/PrimeTTS:F32 # Run inference directly in the terminal: ./build/bin/llama-cli -hf Luigi/PrimeTTS:F32
Use Docker
docker model run hf.co/Luigi/PrimeTTS:F32
- LM Studio
- Jan
- Ollama
How to use Luigi/PrimeTTS with Ollama:
ollama run hf.co/Luigi/PrimeTTS:F32
- Unsloth Studio
How to use Luigi/PrimeTTS with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for Luigi/PrimeTTS to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for Luigi/PrimeTTS to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for Luigi/PrimeTTS to start chatting
- Atomic Chat new
- Docker Model Runner
How to use Luigi/PrimeTTS with Docker Model Runner:
docker model run hf.co/Luigi/PrimeTTS:F32
- Lemonade
How to use Luigi/PrimeTTS with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull Luigi/PrimeTTS:F32
Run and chat with the model
lemonade run user.PrimeTTS-F32
List all available models
lemonade list
PrimeTTS: full training pipeline + weights (fine-tune of Inflect-Nano-v1)
Browse files- README.md +117 -0
- inflect_nano/LICENSE.inflect-nano +201 -0
- inflect_nano/__init__.py +1 -0
- inflect_nano/acoustic.py +1295 -0
- inflect_nano/text_cleaning.py +28 -0
- inflect_nano/vocoder.py +844 -0
- scripts/align_durations_v4.py +181 -0
- scripts/assess_big.py +38 -0
- scripts/export_8k.py +136 -0
- scripts/frontend_bopomofo.py +102 -0
- scripts/gen_breezy_corpus.py +124 -0
- scripts/probe_forced_synth.py +50 -0
- scripts/run_bilingual.sh +55 -0
- scripts/run_v4zh_v2.sh +54 -0
- scripts/run_voc_retrain.sh +14 -0
- scripts/select_diverse_text.py +107 -0
- scripts/symbol_table.json +186 -0
- scripts/synth_from_text.py +68 -0
- scripts/xasr_offline.py +56 -0
- weights/acoustic_decoder.onnx +3 -0
- weights/acoustic_encoder.onnx +3 -0
- weights/acoustic_zh_v2_60k.pt +3 -0
- weights/meta.json +8 -0
- weights/symbol_table.json +186 -0
- weights/vocoder.onnx +3 -0
README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- zh
|
| 5 |
+
- en
|
| 6 |
+
tags:
|
| 7 |
+
- text-to-speech
|
| 8 |
+
- tts
|
| 9 |
+
- onnx
|
| 10 |
+
- on-device
|
| 11 |
+
- jetson
|
| 12 |
+
- telephony
|
| 13 |
+
- mandarin
|
| 14 |
+
- taiwanese-mandarin
|
| 15 |
+
base_model: owensong/Inflect-Nano-v1
|
| 16 |
+
library_name: onnxruntime
|
| 17 |
+
pipeline_tag: text-to-speech
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# PrimeTTS β tiny bilingual zh-TW + English TTS (8 kHz, CPU)
|
| 21 |
+
|
| 22 |
+
**PrimeTTS** is an ultra-small **4.63M-parameter** Mandarin (Taiwan) + English text-to-speech model
|
| 23 |
+
that runs **entirely on CPU** via `onnxruntime` and emits **8 kHz** audio β sized for **G.711
|
| 24 |
+
telephony** and **on-device (Jetson-class)** deployment.
|
| 25 |
+
|
| 26 |
+
It is a **fine-tune of [`owensong/Inflect-Nano-v1`](https://huggingface.co/owensong/Inflect-Nano-v1)**
|
| 27 |
+
(Apache-2.0) β the **same, frozen architecture**, retrained for zh-TW + English. No architecture
|
| 28 |
+
changes, no neural-architecture-search.
|
| 29 |
+
|
| 30 |
+
- **Acoustic** (`MicroFastSpeech`, ~3.47M): FastSpeech-style depthwise Conv-FFN (**no attention**),
|
| 31 |
+
external durations + length regulator, frame-pitch, BiGRU, postnet.
|
| 32 |
+
- **Vocoder** (~1.17M): Snake-HiFiGAN. We added an **8 kHz variant** (`snake_8k`: sr 8000, n_fft 512,
|
| 33 |
+
hop 128, 80 mels) and a **zh-TW + English frontend** (bopomofo + arpabet, one unified phone sequence).
|
| 34 |
+
|
| 35 |
+
**Live demo:** https://huggingface.co/spaces/Luigi/inflect-nano-zhtw-en-8k-demo
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## How these weights were obtained β the two things that matter
|
| 40 |
+
|
| 41 |
+
Inflect-Nano-v1's 4.63M architecture is **not capacity-limited** for this task. We confirmed the
|
| 42 |
+
original English checkpoint scores ~0.05 CER on our eval, yet our first retrains were unintelligible
|
| 43 |
+
(~0.88 CER). The gap was **not** the model size. Two fixes β **architecture frozen** β took held-out
|
| 44 |
+
Mandarin CER from **~0.88 β ~0.06**:
|
| 45 |
+
|
| 46 |
+
1. **Phone-level forced alignment.** FastSpeech needs a per-phone duration for every training clip.
|
| 47 |
+
Crude char/letter-CTC alignment (then splitting a char's span across its phones by heuristic) gives
|
| 48 |
+
*wrong relative phone durations* β the acoustic learns a mis-timed phoneβmel map β over-smoothed,
|
| 49 |
+
garbled output in **every** language. Replace it with **true phone-level forced alignment**
|
| 50 |
+
(`align_durations_v4.py`: the espeak phoneme-CTC model `facebook/wav2vec2-lv-60-espeak-cv-ft` +
|
| 51 |
+
`torchaudio.forced_align`, aligning *your frontend's own phone sequence* mapped to IPA). This alone:
|
| 52 |
+
Mandarin 0.88 β 0.40.
|
| 53 |
+
|
| 54 |
+
2. **Diverse, well-covered training text.** The model can only pronounce characters/words it has seen.
|
| 55 |
+
A narrow corpus (β234 Han chars) left ~39% of held-out characters unseen. Expanding character/word
|
| 56 |
+
coverage (`select_diverse_text.py`: Tatoeba β OpenCC `s2twp` β greedy coverage) took Mandarin
|
| 57 |
+
0.40 β **0.06**. Applied symmetrically to English, the same recipe yields a genuinely **bilingual**
|
| 58 |
+
model (zh-only β 0.13, English β 0.16) in one 4.63M net β no language routing.
|
| 59 |
+
|
| 60 |
+
Everything else (loss weights, steps, etc.) is essentially Inflect-Nano-v1's defaults.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## Reproduce / fine-tune (your own voice or language)
|
| 65 |
+
|
| 66 |
+
```
|
| 67 |
+
scripts/ full pipeline (frontend, aligner, corpus-gen, train, export, eval)
|
| 68 |
+
inflect_nano/ the trainer (acoustic.py + vocoder.py), forked from Inflect-Nano-v1 (LICENSE included)
|
| 69 |
+
weights/ deployable ONNX (encoder/decoder/vocoder) + meta.json + symbol_table.json
|
| 70 |
+
+ acoustic_zh_v2_60k.pt (PyTorch checkpoint, to resume / fine-tune)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
**Step 1 β Teacher corpus.** Synthesize (text β audio) clips in ONE target voice, ASR-gated for
|
| 74 |
+
fidelity. We used **BreezyVoice** (MediaTek, CosyVoice-based; single "mark" voice, does zh + en) with a
|
| 75 |
+
**Breeze-ASR-25** gate (`gen_breezy_corpus.py` β keeps a clip only if its ASR transcript matches the
|
| 76 |
+
intended text, t2s-normalized). Any clean single-speaker TTS/recordings work.
|
| 77 |
+
|
| 78 |
+
**Step 2 β Diverse text.** `python scripts/select_diverse_text.py --lang zh --n 6000 --out zh.tsv`
|
| 79 |
+
(and `--lang en`). Feed the `.tsv` to step 1. Coverage is the single biggest driver of held-out quality.
|
| 80 |
+
|
| 81 |
+
**Step 3 β Phone-level alignment.** `python scripts/align_durations_v4.py --manifest corpus/manifest.jsonl
|
| 82 |
+
--out align.jsonl` β per-phone durations from real audio. **This is the key fix β do not skip it.**
|
| 83 |
+
|
| 84 |
+
**Step 4 β Train acoustic.** `inflect_nano.acoustic --durations-jsonl align.jsonl --vocoder-variant
|
| 85 |
+
snake_8k --sample-rate 8000 ...` (see `scripts/run_bilingual.sh`). Mix languages in one corpus, single
|
| 86 |
+
speaker, for native code-mix. ~60k steps, batch 16.
|
| 87 |
+
|
| 88 |
+
**Step 5 β Train vocoder.** `inflect_nano.vocoder --train-jsonl voc_rows.jsonl --variant snake_8k ...`
|
| 89 |
+
(see `scripts/run_voc_retrain.sh`). Train on the same diverse audio; higher `--stft-weight` = crisper.
|
| 90 |
+
|
| 91 |
+
**Step 6 β Export ONNX.** `python scripts/export_8k.py --acoustic-ckpt <pt> --vocoder-ckpt <pt>
|
| 92 |
+
--out-dir onnx/` β `acoustic_encoder.onnx` β numpy length-regulator (`host_regulate`) β
|
| 93 |
+
`acoustic_decoder.onnx` β `vocoder.onnx`. Fully torch-free at inference.
|
| 94 |
+
|
| 95 |
+
**Step 7 β Evaluate.** `synth_from_text.py` + `assess_big.py` (offline X-ASR CER/WER). Use β₯30 held-out
|
| 96 |
+
sentences β small eval sets are too noisy.
|
| 97 |
+
|
| 98 |
+
### Inference (torch-free, CPU)
|
| 99 |
+
```python
|
| 100 |
+
import frontend_bopomofo as F, numpy as np, onnxruntime as ort
|
| 101 |
+
o = F.text_to_ids("ζ¨ε₯½,ζ‘θΏδ½Ώη¨ PrimeTTSγ") # bopomofo + arpabet -> ids
|
| 102 |
+
# encoder.onnx -> host_regulate (numpy) -> decoder.onnx -> vocoder.onnx -> 8 kHz wav
|
| 103 |
+
```
|
| 104 |
+
See `scripts/synth_from_text.py` for the full ~40-line runtime (also runs as-is on a Jetson Nano CPU).
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## Credits & licenses
|
| 109 |
+
|
| 110 |
+
- **Base model / trainer:** [`owensong/Inflect-Nano-v1`](https://huggingface.co/owensong/Inflect-Nano-v1)
|
| 111 |
+
(Apache-2.0; `inflect_nano/LICENSE.inflect-nano`).
|
| 112 |
+
- **Teacher:** BreezyVoice (MediaTek Research). **Gate ASR:** `Breeze-ASR-25` (MediaTek Research).
|
| 113 |
+
- **Aligner:** `facebook/wav2vec2-lv-60-espeak-cv-ft` + `torchaudio.forced_align`.
|
| 114 |
+
- **Frontend:** `g2pw` (Taiwan bopomofo, polyphone disambiguation) + `g2p_en` (arpabet).
|
| 115 |
+
- **Eval ASR:** sherpa-onnx X-ASR (zh-en Zipformer). **Text:** Tatoeba (CC-BY 2.0 FR).
|
| 116 |
+
|
| 117 |
+
This repository: **Apache-2.0**.
|
inflect_nano/LICENSE.inflect-nano
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.
|
inflect_nano/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Inflect-Nano-v1 runtime package."""
|
inflect_nano/acoustic.py
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import random
|
| 7 |
+
import sys
|
| 8 |
+
import time
|
| 9 |
+
from dataclasses import asdict, dataclass
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from torch import nn
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
import torchaudio
|
| 16 |
+
|
| 17 |
+
SCRIPT_ROOT = Path(__file__).resolve().parent
|
| 18 |
+
PROJECT_ROOT = SCRIPT_ROOT.parents[0]
|
| 19 |
+
FRONTEND_ROOT = PROJECT_ROOT / "third_party" / "tiny_tts_frontend"
|
| 20 |
+
sys.path = [str(FRONTEND_ROOT), str(SCRIPT_ROOT)] + [p for p in sys.path if p]
|
| 21 |
+
|
| 22 |
+
from inflect_nano.vocoder import HifiGanConfig, HifiGanGenerator, MelFrontend, make_config
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class MicroFastSpeechConfig:
|
| 27 |
+
vocab_size: int = 256
|
| 28 |
+
tone_size: int = 16
|
| 29 |
+
lang_size: int = 4
|
| 30 |
+
n_mels: int = 80
|
| 31 |
+
hidden: int = 168
|
| 32 |
+
encoder_layers: int = 5
|
| 33 |
+
decoder_layers: int = 6
|
| 34 |
+
decoder_ff_mult: int = 3
|
| 35 |
+
kernel_size: int = 7
|
| 36 |
+
speaker_count: int = 2
|
| 37 |
+
speaker_dim: int = 64
|
| 38 |
+
dropout: float = 0.08
|
| 39 |
+
sample_rate: int = 24000
|
| 40 |
+
max_frames: int = 1400
|
| 41 |
+
postnet_scale: float = 0.10
|
| 42 |
+
use_frame_pitch: bool = True
|
| 43 |
+
abs_frame_bins: int = 512
|
| 44 |
+
use_contextual_predictors: bool = False
|
| 45 |
+
use_group_duration_planner: bool = False
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def count_parameters(model: nn.Module) -> int:
|
| 49 |
+
return sum(p.numel() for p in model.parameters())
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def load_rows(path: Path, max_rows: int = 0) -> list[dict]:
|
| 53 |
+
rows = []
|
| 54 |
+
with path.open("r", encoding="utf-8") as f:
|
| 55 |
+
for line in f:
|
| 56 |
+
if line.strip():
|
| 57 |
+
row = json.loads(line)
|
| 58 |
+
if Path(str(row.get("target_audio") or "")).is_file():
|
| 59 |
+
rows.append(row)
|
| 60 |
+
if max_rows and len(rows) >= max_rows:
|
| 61 |
+
break
|
| 62 |
+
if not rows:
|
| 63 |
+
raise RuntimeError(f"No usable rows in {path}")
|
| 64 |
+
return rows
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_audio(path: str, sample_rate: int, max_seconds: float) -> torch.Tensor:
|
| 68 |
+
import soundfile as _sf # avoid torchaudio.load (needs torchcodec/ffmpeg on torch>=2.1)
|
| 69 |
+
_a, sr = _sf.read(path, dtype="float32", always_2d=True)
|
| 70 |
+
wav = torch.from_numpy(_a.T) # [ch, T]
|
| 71 |
+
if wav.shape[0] > 1:
|
| 72 |
+
wav = wav.mean(dim=0, keepdim=True)
|
| 73 |
+
if sr != sample_rate:
|
| 74 |
+
wav = torchaudio.functional.resample(wav, sr, sample_rate)
|
| 75 |
+
return wav[:, : int(sample_rate * max_seconds)].squeeze(0).clamp(-1.0, 1.0)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def fit_durations(durations: list[int], target_frames: int) -> list[int]:
|
| 79 |
+
if sum(durations) == target_frames:
|
| 80 |
+
return list(durations)
|
| 81 |
+
total = max(1, sum(durations))
|
| 82 |
+
raw = [max(0.0, d * target_frames / total) for d in durations]
|
| 83 |
+
out = [int(math.floor(x)) for x in raw]
|
| 84 |
+
order = sorted(((raw[i] - out[i], i) for i in range(len(out))), reverse=True)
|
| 85 |
+
for _, idx in order[: max(0, target_frames - sum(out))]:
|
| 86 |
+
out[idx] += 1
|
| 87 |
+
while sum(out) > target_frames:
|
| 88 |
+
idx = max(range(len(out)), key=lambda i: out[i])
|
| 89 |
+
out[idx] -= 1
|
| 90 |
+
return out
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def pad_1d(items: list[torch.Tensor], value: float = 0.0) -> torch.Tensor:
|
| 94 |
+
max_len = max(x.numel() for x in items)
|
| 95 |
+
out = torch.full((len(items), max_len), value, dtype=items[0].dtype)
|
| 96 |
+
for i, item in enumerate(items):
|
| 97 |
+
out[i, : item.numel()] = item
|
| 98 |
+
return out
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def pad_2d(items: list[torch.Tensor], value: float = 0.0) -> torch.Tensor:
|
| 102 |
+
max_len = max(x.shape[0] for x in items)
|
| 103 |
+
dim = items[0].shape[1]
|
| 104 |
+
out = torch.full((len(items), max_len, dim), value, dtype=items[0].dtype)
|
| 105 |
+
for i, item in enumerate(items):
|
| 106 |
+
out[i, : item.shape[0]] = item
|
| 107 |
+
return out
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def pad_mels(items: list[torch.Tensor]) -> tuple[torch.Tensor, torch.Tensor]:
|
| 111 |
+
max_len = max(x.shape[-1] for x in items)
|
| 112 |
+
n_mels = items[0].shape[0]
|
| 113 |
+
out = torch.zeros(len(items), n_mels, max_len, dtype=items[0].dtype)
|
| 114 |
+
mask = torch.zeros(len(items), max_len, dtype=torch.bool)
|
| 115 |
+
for i, mel in enumerate(items):
|
| 116 |
+
frames = mel.shape[-1]
|
| 117 |
+
out[i, :, :frames] = mel
|
| 118 |
+
mask[i, :frames] = True
|
| 119 |
+
return out, mask
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def pad_wavs(items: list[torch.Tensor], frames: list[int], hop_size: int) -> torch.Tensor:
|
| 123 |
+
max_len = max(max(1, int(frame_count)) * hop_size for frame_count in frames)
|
| 124 |
+
out = torch.zeros(len(items), max_len, dtype=items[0].dtype)
|
| 125 |
+
for i, (wav, frame_count) in enumerate(zip(items, frames)):
|
| 126 |
+
length = max(1, int(frame_count)) * hop_size
|
| 127 |
+
cropped = wav[:length]
|
| 128 |
+
out[i, : cropped.numel()] = cropped
|
| 129 |
+
return out
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def aggregate_token_features(mel: torch.Tensor, durations: list[int]) -> tuple[torch.Tensor, torch.Tensor]:
|
| 133 |
+
# mel: [80, frames], log-mel from the exact V2+ frontend.
|
| 134 |
+
frames = mel.shape[-1]
|
| 135 |
+
amp = torch.exp(mel).clamp_min(1e-5)
|
| 136 |
+
energy_frame = mel.mean(dim=0)
|
| 137 |
+
bins = torch.linspace(0.0, 1.0, mel.shape[0], device=mel.device).view(-1, 1)
|
| 138 |
+
bright_frame = (amp * bins).sum(dim=0) / amp.sum(dim=0).clamp_min(1e-5)
|
| 139 |
+
energies = []
|
| 140 |
+
brights = []
|
| 141 |
+
pos = 0
|
| 142 |
+
for dur in durations:
|
| 143 |
+
end = min(frames, pos + max(0, int(dur)))
|
| 144 |
+
if end > pos:
|
| 145 |
+
energies.append(energy_frame[pos:end].mean())
|
| 146 |
+
brights.append(bright_frame[pos:end].mean())
|
| 147 |
+
else:
|
| 148 |
+
energies.append(torch.zeros((), device=mel.device, dtype=mel.dtype))
|
| 149 |
+
brights.append(torch.zeros((), device=mel.device, dtype=mel.dtype))
|
| 150 |
+
pos = end
|
| 151 |
+
return torch.stack(energies), torch.stack(brights)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def aggregate_token_pitch(pitch_frame: torch.Tensor, durations: list[int]) -> torch.Tensor:
|
| 155 |
+
# pitch_frame: [2, frames] with normalized log-f0 and voiced flag.
|
| 156 |
+
frames = pitch_frame.shape[-1]
|
| 157 |
+
out = []
|
| 158 |
+
pos = 0
|
| 159 |
+
for dur in durations:
|
| 160 |
+
end = min(frames, pos + max(0, int(dur)))
|
| 161 |
+
if end > pos:
|
| 162 |
+
span = pitch_frame[:, pos:end]
|
| 163 |
+
voiced = span[1].mean()
|
| 164 |
+
voiced_mask = span[1] > 0.5
|
| 165 |
+
if bool(voiced_mask.any()):
|
| 166 |
+
log_f0 = span[0, voiced_mask].mean()
|
| 167 |
+
else:
|
| 168 |
+
log_f0 = torch.zeros((), dtype=pitch_frame.dtype)
|
| 169 |
+
out.append(torch.stack([log_f0, voiced]))
|
| 170 |
+
else:
|
| 171 |
+
out.append(torch.zeros(2, dtype=pitch_frame.dtype))
|
| 172 |
+
pos = end
|
| 173 |
+
return torch.stack(out, dim=0)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def extract_pitch_features(wav: torch.Tensor, sample_rate: int, frames: int) -> torch.Tensor:
|
| 177 |
+
# Returns [2, frames]: normalized log-f0 and voiced flag. The detector can
|
| 178 |
+
# produce octave spikes, so clip to speech range and median-smooth lightly.
|
| 179 |
+
pitch = torchaudio.functional.detect_pitch_frequency(
|
| 180 |
+
wav.unsqueeze(0).cpu(),
|
| 181 |
+
sample_rate,
|
| 182 |
+
frame_time=256 / sample_rate,
|
| 183 |
+
).squeeze(0)
|
| 184 |
+
if pitch.numel() < frames:
|
| 185 |
+
pitch = F.pad(pitch, (0, frames - pitch.numel()), value=0.0)
|
| 186 |
+
pitch = pitch[:frames]
|
| 187 |
+
voiced = ((pitch >= 55.0) & (pitch <= 420.0)).float()
|
| 188 |
+
pitch = pitch.clamp(55.0, 420.0)
|
| 189 |
+
# Median filter over 5 frames to reduce spurious jumps.
|
| 190 |
+
if pitch.numel() >= 5:
|
| 191 |
+
padded = F.pad(pitch.view(1, 1, -1), (2, 2), mode="replicate")
|
| 192 |
+
windows = padded.unfold(-1, 5, 1).squeeze(0).squeeze(0)
|
| 193 |
+
pitch = windows.median(dim=-1).values
|
| 194 |
+
log_f0 = (torch.log(pitch) - math.log(140.0)) / 0.45
|
| 195 |
+
log_f0 = log_f0.clamp(-3.0, 3.0) * voiced
|
| 196 |
+
return torch.stack([log_f0, voiced], dim=0)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class ConvFFNBlock(nn.Module):
|
| 200 |
+
def __init__(self, hidden: int, kernel_size: int, dropout: float, ff_mult: int = 4) -> None:
|
| 201 |
+
super().__init__()
|
| 202 |
+
pad = kernel_size // 2
|
| 203 |
+
self.norm1 = nn.LayerNorm(hidden)
|
| 204 |
+
self.depth = nn.Conv1d(hidden, hidden * 2, kernel_size, padding=pad, groups=hidden)
|
| 205 |
+
self.point = nn.Conv1d(hidden, hidden, 1)
|
| 206 |
+
self.drop = nn.Dropout(dropout)
|
| 207 |
+
self.norm2 = nn.LayerNorm(hidden)
|
| 208 |
+
self.ff = nn.Sequential(
|
| 209 |
+
nn.Linear(hidden, hidden * ff_mult),
|
| 210 |
+
nn.SiLU(),
|
| 211 |
+
nn.Dropout(dropout),
|
| 212 |
+
nn.Linear(hidden * ff_mult, hidden),
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 216 |
+
y = self.norm1(x).transpose(1, 2)
|
| 217 |
+
a, b = self.depth(y).chunk(2, dim=1)
|
| 218 |
+
y = self.point(a * torch.sigmoid(b)).transpose(1, 2)
|
| 219 |
+
x = x + self.drop(y)
|
| 220 |
+
x = x + self.drop(self.ff(self.norm2(x)))
|
| 221 |
+
if mask is not None:
|
| 222 |
+
x = x * mask.unsqueeze(-1)
|
| 223 |
+
return x
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
class MicroFastSpeech(nn.Module):
|
| 227 |
+
def __init__(self, cfg: MicroFastSpeechConfig) -> None:
|
| 228 |
+
super().__init__()
|
| 229 |
+
self.cfg = cfg
|
| 230 |
+
# Phone id 0 is a real inserted blank/silence token from TinyTTS, not
|
| 231 |
+
# padding. Padding is tracked by duration masks instead.
|
| 232 |
+
self.phone = nn.Embedding(cfg.vocab_size, cfg.hidden)
|
| 233 |
+
self.tone = nn.Embedding(cfg.tone_size, cfg.hidden)
|
| 234 |
+
self.lang = nn.Embedding(cfg.lang_size, cfg.hidden)
|
| 235 |
+
self.speaker = nn.Embedding(cfg.speaker_count, cfg.speaker_dim)
|
| 236 |
+
self.speaker_proj = nn.Linear(cfg.speaker_dim, cfg.hidden)
|
| 237 |
+
self.encoder = nn.ModuleList([ConvFFNBlock(cfg.hidden, cfg.kernel_size, cfg.dropout) for _ in range(cfg.encoder_layers)])
|
| 238 |
+
self.duration_head = nn.Sequential(nn.LayerNorm(cfg.hidden), nn.Linear(cfg.hidden, cfg.hidden), nn.SiLU(), nn.Linear(cfg.hidden, 1))
|
| 239 |
+
self.energy_head = nn.Sequential(nn.LayerNorm(cfg.hidden), nn.Linear(cfg.hidden, cfg.hidden // 2), nn.SiLU(), nn.Linear(cfg.hidden // 2, 1))
|
| 240 |
+
self.bright_head = nn.Sequential(nn.LayerNorm(cfg.hidden), nn.Linear(cfg.hidden, cfg.hidden // 2), nn.SiLU(), nn.Linear(cfg.hidden // 2, 1))
|
| 241 |
+
self.pitch_head = nn.Sequential(nn.LayerNorm(cfg.hidden), nn.Linear(cfg.hidden, cfg.hidden), nn.SiLU(), nn.Linear(cfg.hidden, 2))
|
| 242 |
+
self.group_duration_delta = nn.Linear(cfg.hidden, 1) if cfg.use_group_duration_planner else None
|
| 243 |
+
if self.group_duration_delta is not None:
|
| 244 |
+
nn.init.zeros_(self.group_duration_delta.weight)
|
| 245 |
+
nn.init.zeros_(self.group_duration_delta.bias)
|
| 246 |
+
self.predictor_context = (
|
| 247 |
+
ConvFFNBlock(cfg.hidden, 5, cfg.dropout, 2) if cfg.use_contextual_predictors else nn.Identity()
|
| 248 |
+
)
|
| 249 |
+
self.duration_delta = nn.Linear(cfg.hidden, 1) if cfg.use_contextual_predictors else None
|
| 250 |
+
self.energy_delta = nn.Linear(cfg.hidden, 1) if cfg.use_contextual_predictors else None
|
| 251 |
+
self.bright_delta = nn.Linear(cfg.hidden, 1) if cfg.use_contextual_predictors else None
|
| 252 |
+
self.pitch_delta = nn.Linear(cfg.hidden, 2) if cfg.use_contextual_predictors else None
|
| 253 |
+
if cfg.use_contextual_predictors:
|
| 254 |
+
for layer in (self.duration_delta, self.energy_delta, self.bright_delta, self.pitch_delta):
|
| 255 |
+
nn.init.zeros_(layer.weight)
|
| 256 |
+
nn.init.zeros_(layer.bias)
|
| 257 |
+
self.energy_proj = nn.Linear(1, cfg.hidden)
|
| 258 |
+
self.bright_proj = nn.Linear(1, cfg.hidden)
|
| 259 |
+
self.pitch_proj = nn.Sequential(nn.Linear(2, cfg.hidden), nn.SiLU(), nn.Linear(cfg.hidden, cfg.hidden))
|
| 260 |
+
self.abs_frame = nn.Embedding(cfg.abs_frame_bins, cfg.hidden)
|
| 261 |
+
self.frame_proj = nn.Sequential(nn.Linear(8, cfg.hidden), nn.SiLU(), nn.Linear(cfg.hidden, cfg.hidden))
|
| 262 |
+
self.local_ctx = nn.Sequential(
|
| 263 |
+
nn.Linear(cfg.hidden * 3, cfg.hidden * 2),
|
| 264 |
+
nn.SiLU(),
|
| 265 |
+
nn.Linear(cfg.hidden * 2, cfg.hidden),
|
| 266 |
+
)
|
| 267 |
+
self.decoder = nn.ModuleList([ConvFFNBlock(cfg.hidden, cfg.kernel_size, cfg.dropout, cfg.decoder_ff_mult) for _ in range(cfg.decoder_layers)])
|
| 268 |
+
self.frame_gru = nn.GRU(cfg.hidden, cfg.hidden // 2, num_layers=1, batch_first=True, bidirectional=True)
|
| 269 |
+
self.mel_head = nn.Sequential(nn.LayerNorm(cfg.hidden), nn.Linear(cfg.hidden, cfg.hidden), nn.SiLU(), nn.Linear(cfg.hidden, cfg.n_mels))
|
| 270 |
+
self.postnet = nn.Sequential(
|
| 271 |
+
nn.Conv1d(cfg.n_mels, cfg.hidden, 5, padding=2),
|
| 272 |
+
nn.Tanh(),
|
| 273 |
+
nn.Conv1d(cfg.hidden, cfg.hidden, 5, padding=2),
|
| 274 |
+
nn.Tanh(),
|
| 275 |
+
nn.Conv1d(cfg.hidden, cfg.n_mels, 5, padding=2),
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
def encode(self, phone: torch.Tensor, tone: torch.Tensor, lang: torch.Tensor, speaker: torch.Tensor, token_mask: torch.Tensor) -> torch.Tensor:
|
| 279 |
+
x = self.phone(phone) + self.tone(tone.clamp_max(self.cfg.tone_size - 1)) + self.lang(lang.clamp_max(self.cfg.lang_size - 1))
|
| 280 |
+
x = x + self.speaker_proj(self.speaker(speaker)).unsqueeze(1)
|
| 281 |
+
x = x * token_mask.unsqueeze(-1)
|
| 282 |
+
for block in self.encoder:
|
| 283 |
+
x = block(x, token_mask)
|
| 284 |
+
return x
|
| 285 |
+
|
| 286 |
+
def regulate(self, encoded: torch.Tensor, durations: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 287 |
+
batch_frames = []
|
| 288 |
+
batch_meta = []
|
| 289 |
+
lengths = []
|
| 290 |
+
device = encoded.device
|
| 291 |
+
for b in range(encoded.shape[0]):
|
| 292 |
+
reps = []
|
| 293 |
+
meta = []
|
| 294 |
+
durs = durations[b].long().clamp_min(0)
|
| 295 |
+
token_count = max(1, int((durs > 0).sum().item()))
|
| 296 |
+
for i, dur_t in enumerate(durs.tolist()):
|
| 297 |
+
dur = int(dur_t)
|
| 298 |
+
if dur <= 0:
|
| 299 |
+
continue
|
| 300 |
+
reps.append(encoded[b, i].view(1, -1).expand(dur, -1))
|
| 301 |
+
rel = torch.linspace(0.0, 1.0, dur, device=device)
|
| 302 |
+
token_pos = torch.full((dur,), i / max(1, token_count - 1), device=device)
|
| 303 |
+
log_dur = torch.full((dur,), math.log1p(dur) / 6.0, device=device)
|
| 304 |
+
inv_rel = 1.0 - rel
|
| 305 |
+
center = 1.0 - torch.abs(rel * 2.0 - 1.0)
|
| 306 |
+
meta.append(
|
| 307 |
+
torch.stack(
|
| 308 |
+
[
|
| 309 |
+
rel,
|
| 310 |
+
inv_rel,
|
| 311 |
+
center,
|
| 312 |
+
torch.sin(rel * math.pi),
|
| 313 |
+
torch.cos(rel * math.pi),
|
| 314 |
+
token_pos,
|
| 315 |
+
log_dur,
|
| 316 |
+
torch.full_like(rel, dur / 40.0),
|
| 317 |
+
],
|
| 318 |
+
dim=-1,
|
| 319 |
+
)
|
| 320 |
+
)
|
| 321 |
+
if reps:
|
| 322 |
+
frames = torch.cat(reps, dim=0)
|
| 323 |
+
frame_meta = torch.cat(meta, dim=0)
|
| 324 |
+
else:
|
| 325 |
+
frames = encoded[b, :1]
|
| 326 |
+
frame_meta = torch.zeros(1, 8, device=device)
|
| 327 |
+
batch_frames.append(frames[: self.cfg.max_frames])
|
| 328 |
+
batch_meta.append(frame_meta[: self.cfg.max_frames])
|
| 329 |
+
lengths.append(min(frames.shape[0], self.cfg.max_frames))
|
| 330 |
+
max_len = max(lengths)
|
| 331 |
+
out = torch.zeros(encoded.shape[0], max_len, encoded.shape[-1], device=device)
|
| 332 |
+
meta_out = torch.zeros(encoded.shape[0], max_len, 8, device=device)
|
| 333 |
+
mask = torch.zeros(encoded.shape[0], max_len, dtype=torch.bool, device=device)
|
| 334 |
+
for b, frames in enumerate(batch_frames):
|
| 335 |
+
n = min(frames.shape[0], max_len)
|
| 336 |
+
out[b, :n] = frames[:n]
|
| 337 |
+
meta_out[b, :n] = batch_meta[b][:n]
|
| 338 |
+
mask[b, :n] = True
|
| 339 |
+
return out, meta_out, mask
|
| 340 |
+
|
| 341 |
+
def add_local_context(self, encoded: torch.Tensor, durations: torch.Tensor) -> torch.Tensor:
|
| 342 |
+
device = encoded.device
|
| 343 |
+
batch_frames = []
|
| 344 |
+
for b in range(encoded.shape[0]):
|
| 345 |
+
reps = []
|
| 346 |
+
durs = durations[b].long().clamp_min(0)
|
| 347 |
+
for i, dur_t in enumerate(durs.tolist()):
|
| 348 |
+
dur = int(dur_t)
|
| 349 |
+
if dur <= 0:
|
| 350 |
+
continue
|
| 351 |
+
prev_i = max(0, i - 1)
|
| 352 |
+
next_i = min(encoded.shape[1] - 1, i + 1)
|
| 353 |
+
ctx = torch.cat([encoded[b, prev_i], encoded[b, i], encoded[b, next_i]], dim=-1)
|
| 354 |
+
reps.append(ctx.view(1, -1).expand(dur, -1))
|
| 355 |
+
if reps:
|
| 356 |
+
frames = torch.cat(reps, dim=0)
|
| 357 |
+
else:
|
| 358 |
+
frames = torch.zeros(1, encoded.shape[-1] * 3, device=device)
|
| 359 |
+
batch_frames.append(frames[: self.cfg.max_frames])
|
| 360 |
+
max_len = max(x.shape[0] for x in batch_frames)
|
| 361 |
+
ctx_out = torch.zeros(encoded.shape[0], max_len, encoded.shape[-1] * 3, device=device)
|
| 362 |
+
for b, frames in enumerate(batch_frames):
|
| 363 |
+
ctx_out[b, : frames.shape[0]] = frames
|
| 364 |
+
return self.local_ctx(ctx_out)
|
| 365 |
+
|
| 366 |
+
def expand_token_feature(self, feature: torch.Tensor, durations: torch.Tensor) -> torch.Tensor:
|
| 367 |
+
device = feature.device
|
| 368 |
+
batch_frames = []
|
| 369 |
+
for b in range(feature.shape[0]):
|
| 370 |
+
reps = []
|
| 371 |
+
durs = durations[b].long().clamp_min(0)
|
| 372 |
+
for i, dur_t in enumerate(durs.tolist()):
|
| 373 |
+
dur = int(dur_t)
|
| 374 |
+
if dur <= 0:
|
| 375 |
+
continue
|
| 376 |
+
reps.append(feature[b, i].view(1, -1).expand(dur, -1))
|
| 377 |
+
if reps:
|
| 378 |
+
frames = torch.cat(reps, dim=0)
|
| 379 |
+
else:
|
| 380 |
+
frames = torch.zeros(1, feature.shape[-1], device=device)
|
| 381 |
+
batch_frames.append(frames[: self.cfg.max_frames])
|
| 382 |
+
max_len = max(x.shape[0] for x in batch_frames)
|
| 383 |
+
out = torch.zeros(feature.shape[0], max_len, feature.shape[-1], device=device)
|
| 384 |
+
for b, frames in enumerate(batch_frames):
|
| 385 |
+
out[b, : frames.shape[0]] = frames
|
| 386 |
+
return out
|
| 387 |
+
|
| 388 |
+
def forward(
|
| 389 |
+
self,
|
| 390 |
+
phone: torch.Tensor,
|
| 391 |
+
tone: torch.Tensor,
|
| 392 |
+
lang: torch.Tensor,
|
| 393 |
+
speaker: torch.Tensor,
|
| 394 |
+
durations: torch.Tensor,
|
| 395 |
+
energy_target: torch.Tensor | None = None,
|
| 396 |
+
bright_target: torch.Tensor | None = None,
|
| 397 |
+
pitch_frame: torch.Tensor | None = None,
|
| 398 |
+
predicted_prosody_mix: float = 0.0,
|
| 399 |
+
detach_mixed_predictions: bool = True,
|
| 400 |
+
) -> dict[str, torch.Tensor]:
|
| 401 |
+
token_mask = durations.gt(0)
|
| 402 |
+
encoded = self.encode(phone, tone, lang, speaker, token_mask)
|
| 403 |
+
log_dur, energy_pred, bright_pred, pitch_pred = self.predict_prosody(encoded, token_mask)
|
| 404 |
+
mixed_energy_pred = energy_pred.detach() if detach_mixed_predictions else energy_pred
|
| 405 |
+
mixed_bright_pred = bright_pred.detach() if detach_mixed_predictions else bright_pred
|
| 406 |
+
if energy_target is not None:
|
| 407 |
+
energy = torch.lerp(energy_target, mixed_energy_pred, predicted_prosody_mix)
|
| 408 |
+
else:
|
| 409 |
+
energy = energy_pred
|
| 410 |
+
if bright_target is not None:
|
| 411 |
+
bright = torch.lerp(bright_target, mixed_bright_pred, predicted_prosody_mix)
|
| 412 |
+
else:
|
| 413 |
+
bright = bright_pred
|
| 414 |
+
conditioned = encoded + self.energy_proj(energy.unsqueeze(-1)) + self.bright_proj(bright.unsqueeze(-1))
|
| 415 |
+
frames, frame_meta, frame_mask = self.regulate(conditioned, durations)
|
| 416 |
+
x = frames + self.frame_proj(frame_meta) + self.add_local_context(conditioned, durations)
|
| 417 |
+
pos = torch.arange(x.shape[1], device=x.device)
|
| 418 |
+
pos = torch.div(pos * self.cfg.abs_frame_bins, max(1, self.cfg.max_frames), rounding_mode="floor").clamp_max(
|
| 419 |
+
self.cfg.abs_frame_bins - 1
|
| 420 |
+
)
|
| 421 |
+
x = x + self.abs_frame(pos).unsqueeze(0)
|
| 422 |
+
if self.cfg.use_frame_pitch:
|
| 423 |
+
if pitch_frame is not None:
|
| 424 |
+
pitch_frame = pitch_frame[:, :, : x.shape[1]].transpose(1, 2)
|
| 425 |
+
if pitch_frame.shape[1] < x.shape[1]:
|
| 426 |
+
pitch_frame = F.pad(pitch_frame, (0, 0, 0, x.shape[1] - pitch_frame.shape[1]))
|
| 427 |
+
if predicted_prosody_mix > 0.0:
|
| 428 |
+
mixed_pitch_pred = pitch_pred.detach() if detach_mixed_predictions else pitch_pred
|
| 429 |
+
predicted_pitch_frame = self.expand_token_feature(mixed_pitch_pred, durations)[:, : x.shape[1]]
|
| 430 |
+
pitch_frame = torch.lerp(pitch_frame, predicted_pitch_frame, predicted_prosody_mix)
|
| 431 |
+
else:
|
| 432 |
+
pitch_frame = self.expand_token_feature(pitch_pred, durations)[:, : x.shape[1]]
|
| 433 |
+
x = x + self.pitch_proj(pitch_frame)
|
| 434 |
+
for block in self.decoder:
|
| 435 |
+
x = block(x, frame_mask)
|
| 436 |
+
x = x + self.frame_gru(x)[0]
|
| 437 |
+
mel = self.mel_head(x).transpose(1, 2)
|
| 438 |
+
mel = mel + self.cfg.postnet_scale * self.postnet(mel)
|
| 439 |
+
group_log_dur, group_mask = self.group_log_durations(phone, log_dur, encoded)
|
| 440 |
+
return {
|
| 441 |
+
"mel": mel,
|
| 442 |
+
"frame_mask": frame_mask,
|
| 443 |
+
"log_dur": log_dur,
|
| 444 |
+
"group_log_dur": group_log_dur,
|
| 445 |
+
"group_mask": group_mask,
|
| 446 |
+
"energy": energy_pred,
|
| 447 |
+
"bright": bright_pred,
|
| 448 |
+
"pitch": pitch_pred,
|
| 449 |
+
"token_mask": token_mask,
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
def predict_prosody(
|
| 453 |
+
self, encoded: torch.Tensor, token_mask: torch.Tensor
|
| 454 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 455 |
+
log_dur = self.duration_head(encoded).squeeze(-1)
|
| 456 |
+
energy = self.energy_head(encoded).squeeze(-1)
|
| 457 |
+
bright = self.bright_head(encoded).squeeze(-1)
|
| 458 |
+
pitch = self.pitch_head(encoded)
|
| 459 |
+
if self.cfg.use_contextual_predictors:
|
| 460 |
+
context = self.predictor_context(encoded, token_mask)
|
| 461 |
+
log_dur = log_dur + self.duration_delta(context).squeeze(-1)
|
| 462 |
+
energy = energy + self.energy_delta(context).squeeze(-1)
|
| 463 |
+
bright = bright + self.bright_delta(context).squeeze(-1)
|
| 464 |
+
pitch = pitch + self.pitch_delta(context)
|
| 465 |
+
return log_dur, energy, bright, pitch
|
| 466 |
+
|
| 467 |
+
def group_log_durations(
|
| 468 |
+
self, phone: torch.Tensor, log_dur: torch.Tensor, encoded: torch.Tensor
|
| 469 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 470 |
+
"""Predict stable blank-plus-phone region durations at visible phones."""
|
| 471 |
+
base_dur = torch.expm1(log_dur).clamp_min(0.05)
|
| 472 |
+
grouped = torch.zeros_like(log_dur)
|
| 473 |
+
group_mask = torch.zeros_like(phone, dtype=torch.bool)
|
| 474 |
+
delta = self.group_duration_delta(encoded).squeeze(-1) if self.group_duration_delta is not None else None
|
| 475 |
+
for batch_index in range(phone.shape[0]):
|
| 476 |
+
pending: list[torch.Tensor] = []
|
| 477 |
+
last_visible: int | None = None
|
| 478 |
+
for token_index in range(phone.shape[1]):
|
| 479 |
+
pending.append(base_dur[batch_index, token_index])
|
| 480 |
+
if int(phone[batch_index, token_index].item()) != 0:
|
| 481 |
+
value = torch.stack(pending).sum()
|
| 482 |
+
if delta is not None:
|
| 483 |
+
value = value * torch.exp(delta[batch_index, token_index].clamp(-1.5, 1.5))
|
| 484 |
+
grouped[batch_index, token_index] = torch.log1p(value)
|
| 485 |
+
group_mask[batch_index, token_index] = True
|
| 486 |
+
pending = []
|
| 487 |
+
last_visible = token_index
|
| 488 |
+
if pending and last_visible is not None:
|
| 489 |
+
value = torch.expm1(grouped[batch_index, last_visible]) + torch.stack(pending).sum()
|
| 490 |
+
grouped[batch_index, last_visible] = torch.log1p(value)
|
| 491 |
+
return grouped, group_mask
|
| 492 |
+
|
| 493 |
+
def apply_group_duration_plan(
|
| 494 |
+
self, phone: torch.Tensor, log_dur: torch.Tensor, encoded: torch.Tensor, length_scale: float, max_duration: int
|
| 495 |
+
) -> torch.Tensor:
|
| 496 |
+
base = torch.expm1(log_dur).clamp_min(0.05)
|
| 497 |
+
group_log, _ = self.group_log_durations(phone, log_dur, encoded)
|
| 498 |
+
planned = torch.zeros_like(base, dtype=torch.long)
|
| 499 |
+
for batch_index in range(phone.shape[0]):
|
| 500 |
+
pending: list[int] = []
|
| 501 |
+
last_visible: int | None = None
|
| 502 |
+
for token_index in range(phone.shape[1]):
|
| 503 |
+
pending.append(token_index)
|
| 504 |
+
if int(phone[batch_index, token_index].item()) != 0:
|
| 505 |
+
target = max(len(pending), int(round(float(torch.expm1(group_log[batch_index, token_index]) * length_scale))))
|
| 506 |
+
weights = base[batch_index, pending]
|
| 507 |
+
remaining = target - len(pending)
|
| 508 |
+
raw = weights / weights.sum().clamp_min(1e-6) * remaining
|
| 509 |
+
allocated = torch.ones_like(raw, dtype=torch.long) + torch.floor(raw).long()
|
| 510 |
+
remainder = target - int(allocated.sum().item())
|
| 511 |
+
if remainder > 0:
|
| 512 |
+
order = torch.argsort(raw - torch.floor(raw), descending=True)
|
| 513 |
+
allocated[order[:remainder]] += 1
|
| 514 |
+
planned[batch_index, pending] = allocated
|
| 515 |
+
pending = []
|
| 516 |
+
last_visible = token_index
|
| 517 |
+
if pending and last_visible is not None:
|
| 518 |
+
planned[batch_index, last_visible] += max(1, int(round(float(base[batch_index, pending].sum() * length_scale))))
|
| 519 |
+
return planned.clamp(0, max_duration)
|
| 520 |
+
|
| 521 |
+
@torch.no_grad()
|
| 522 |
+
def infer(
|
| 523 |
+
self,
|
| 524 |
+
phone: torch.Tensor,
|
| 525 |
+
tone: torch.Tensor,
|
| 526 |
+
lang: torch.Tensor,
|
| 527 |
+
speaker: torch.Tensor,
|
| 528 |
+
length_scale: float = 1.0,
|
| 529 |
+
min_duration: int = 1,
|
| 530 |
+
max_duration: int = 80,
|
| 531 |
+
pitch_scale: float = 1.0,
|
| 532 |
+
energy_scale: float = 1.0,
|
| 533 |
+
smooth_predictors: bool = False,
|
| 534 |
+
) -> torch.Tensor:
|
| 535 |
+
# In single-sample inference there is no padded tail; id 0 remains the
|
| 536 |
+
# explicit blank/pause token and must keep duration.
|
| 537 |
+
token_mask = torch.ones_like(phone, dtype=torch.bool)
|
| 538 |
+
encoded = self.encode(phone, tone, lang, speaker, token_mask)
|
| 539 |
+
log_dur, energy, bright, pitch = self.predict_prosody(encoded, token_mask)
|
| 540 |
+
if self.group_duration_delta is not None:
|
| 541 |
+
durations = self.apply_group_duration_plan(phone, log_dur, encoded, length_scale, max_duration)
|
| 542 |
+
durations = durations.masked_fill(~token_mask, 0)
|
| 543 |
+
else:
|
| 544 |
+
pred_dur = torch.expm1(log_dur).clamp(0, max_duration) * length_scale
|
| 545 |
+
durations = torch.round(pred_dur).long().clamp_min(min_duration).masked_fill(~token_mask, 0)
|
| 546 |
+
energy = energy * energy_scale
|
| 547 |
+
pitch = torch.stack([pitch[..., 0] * pitch_scale, pitch[..., 1].clamp(0.0, 1.0)], dim=-1)
|
| 548 |
+
if smooth_predictors and phone.shape[1] >= 3:
|
| 549 |
+
energy = F.avg_pool1d(energy.unsqueeze(1), 3, stride=1, padding=1).squeeze(1)
|
| 550 |
+
bright = F.avg_pool1d(bright.unsqueeze(1), 3, stride=1, padding=1).squeeze(1)
|
| 551 |
+
pitch_t = pitch.transpose(1, 2)
|
| 552 |
+
pitch = F.avg_pool1d(pitch_t, 3, stride=1, padding=1).transpose(1, 2)
|
| 553 |
+
conditioned = encoded + self.energy_proj(energy.unsqueeze(-1)) + self.bright_proj(bright.unsqueeze(-1))
|
| 554 |
+
frames, frame_meta, frame_mask = self.regulate(conditioned, durations)
|
| 555 |
+
x = frames + self.frame_proj(frame_meta) + self.add_local_context(conditioned, durations)
|
| 556 |
+
pos = torch.arange(x.shape[1], device=x.device)
|
| 557 |
+
pos = torch.div(pos * self.cfg.abs_frame_bins, max(1, self.cfg.max_frames), rounding_mode="floor").clamp_max(
|
| 558 |
+
self.cfg.abs_frame_bins - 1
|
| 559 |
+
)
|
| 560 |
+
x = x + self.abs_frame(pos).unsqueeze(0)
|
| 561 |
+
if self.cfg.use_frame_pitch:
|
| 562 |
+
pitch_frame = self.expand_token_feature(pitch, durations)[:, : x.shape[1]]
|
| 563 |
+
x = x + self.pitch_proj(pitch_frame)
|
| 564 |
+
for block in self.decoder:
|
| 565 |
+
x = block(x, frame_mask)
|
| 566 |
+
x = x + self.frame_gru(x)[0]
|
| 567 |
+
mel = self.mel_head(x).transpose(1, 2)
|
| 568 |
+
mel = mel + self.cfg.postnet_scale * self.postnet(mel)
|
| 569 |
+
return mel
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def collate(batch: list[dict], cfg: MicroFastSpeechConfig, mel_frontend: MelFrontend, device: torch.device, max_seconds: float, hop_size: int):
|
| 573 |
+
phones = [torch.LongTensor(x["phone_ids"]) for x in batch]
|
| 574 |
+
tones = [torch.LongTensor(x["tone_ids"]) for x in batch]
|
| 575 |
+
langs = [torch.LongTensor(x["lang_ids"]) for x in batch]
|
| 576 |
+
durations_raw = [list(map(int, x["hifigan_durations"])) for x in batch]
|
| 577 |
+
speakers = torch.LongTensor([int(x["speaker_id"]) for x in batch])
|
| 578 |
+
phone = pad_1d(phones, 0).long()
|
| 579 |
+
tone = pad_1d(tones, 0).long()
|
| 580 |
+
lang = pad_1d(langs, 0).long()
|
| 581 |
+
mels = []
|
| 582 |
+
durations = []
|
| 583 |
+
energies = []
|
| 584 |
+
brights = []
|
| 585 |
+
pitches = []
|
| 586 |
+
token_pitches = []
|
| 587 |
+
wavs = []
|
| 588 |
+
frame_counts = []
|
| 589 |
+
with torch.no_grad():
|
| 590 |
+
for row, dur in zip(batch, durations_raw):
|
| 591 |
+
wav_1d = load_audio(str(row["target_audio"]), cfg.sample_rate, max_seconds)
|
| 592 |
+
wav = wav_1d.unsqueeze(0).to(device)
|
| 593 |
+
mel = mel_frontend(wav).squeeze(0).detach().cpu()
|
| 594 |
+
dur = fit_durations(dur[: len(row["phone_ids"])], min(mel.shape[-1], cfg.max_frames))
|
| 595 |
+
mel = mel[:, : sum(dur)]
|
| 596 |
+
energy, bright = aggregate_token_features(mel, dur)
|
| 597 |
+
pitch = extract_pitch_features(wav_1d, cfg.sample_rate, mel.shape[-1])
|
| 598 |
+
token_pitch = aggregate_token_pitch(pitch, dur)
|
| 599 |
+
mels.append(mel)
|
| 600 |
+
durations.append(torch.LongTensor(dur))
|
| 601 |
+
energies.append(energy)
|
| 602 |
+
brights.append(bright)
|
| 603 |
+
pitches.append(pitch)
|
| 604 |
+
token_pitches.append(token_pitch)
|
| 605 |
+
wavs.append(wav_1d)
|
| 606 |
+
frame_counts.append(mel.shape[-1])
|
| 607 |
+
duration = pad_1d(durations, 0).long()
|
| 608 |
+
energy = pad_1d(energies, 0.0).float()
|
| 609 |
+
bright = pad_1d(brights, 0.0).float()
|
| 610 |
+
token_pitch = pad_2d(token_pitches, 0.0).float()
|
| 611 |
+
target_mel, frame_mask = pad_mels(mels)
|
| 612 |
+
pitch_frame, _ = pad_mels(pitches)
|
| 613 |
+
target_wav = pad_wavs(wavs, frame_counts, hop_size)
|
| 614 |
+
return (
|
| 615 |
+
phone.to(device),
|
| 616 |
+
tone.to(device),
|
| 617 |
+
lang.to(device),
|
| 618 |
+
speakers.to(device),
|
| 619 |
+
duration.to(device),
|
| 620 |
+
energy.to(device),
|
| 621 |
+
bright.to(device),
|
| 622 |
+
token_pitch.to(device),
|
| 623 |
+
target_mel.to(device),
|
| 624 |
+
frame_mask.to(device),
|
| 625 |
+
pitch_frame.to(device),
|
| 626 |
+
target_wav.to(device),
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def prepare_row_features(
|
| 631 |
+
row: dict,
|
| 632 |
+
cfg: MicroFastSpeechConfig,
|
| 633 |
+
mel_frontend: MelFrontend,
|
| 634 |
+
device: torch.device,
|
| 635 |
+
max_seconds: float,
|
| 636 |
+
) -> dict:
|
| 637 |
+
dur = list(map(int, row["hifigan_durations"]))
|
| 638 |
+
wav_1d = load_audio(str(row["target_audio"]), cfg.sample_rate, max_seconds)
|
| 639 |
+
with torch.no_grad():
|
| 640 |
+
wav = wav_1d.unsqueeze(0).to(device)
|
| 641 |
+
mel = mel_frontend(wav).squeeze(0).detach().cpu()
|
| 642 |
+
dur = fit_durations(dur[: len(row["phone_ids"])], min(mel.shape[-1], cfg.max_frames))
|
| 643 |
+
mel = mel[:, : sum(dur)]
|
| 644 |
+
energy, bright = aggregate_token_features(mel, dur)
|
| 645 |
+
pitch = extract_pitch_features(wav_1d, cfg.sample_rate, mel.shape[-1])
|
| 646 |
+
token_pitch = aggregate_token_pitch(pitch, dur)
|
| 647 |
+
return {
|
| 648 |
+
"phone": torch.LongTensor(row["phone_ids"]),
|
| 649 |
+
"tone": torch.LongTensor(row["tone_ids"]),
|
| 650 |
+
"lang": torch.LongTensor(row["lang_ids"]),
|
| 651 |
+
"speaker": int(row["speaker_id"]),
|
| 652 |
+
"duration": torch.LongTensor(dur),
|
| 653 |
+
"energy": energy.float(),
|
| 654 |
+
"bright": bright.float(),
|
| 655 |
+
"token_pitch": token_pitch.float(),
|
| 656 |
+
"target_mel": mel.float(),
|
| 657 |
+
"pitch_frame": pitch.float(),
|
| 658 |
+
"target_wav": wav_1d.float(),
|
| 659 |
+
"frame_count": int(mel.shape[-1]),
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def collate_prepared(batch: list[dict], device: torch.device, hop_size: int):
|
| 664 |
+
phone = pad_1d([x["phone"] for x in batch], 0).long()
|
| 665 |
+
tone = pad_1d([x["tone"] for x in batch], 0).long()
|
| 666 |
+
lang = pad_1d([x["lang"] for x in batch], 0).long()
|
| 667 |
+
speakers = torch.LongTensor([int(x["speaker"]) for x in batch])
|
| 668 |
+
duration = pad_1d([x["duration"] for x in batch], 0).long()
|
| 669 |
+
energy = pad_1d([x["energy"] for x in batch], 0.0).float()
|
| 670 |
+
bright = pad_1d([x["bright"] for x in batch], 0.0).float()
|
| 671 |
+
token_pitch = pad_2d([x["token_pitch"] for x in batch], 0.0).float()
|
| 672 |
+
target_mel, frame_mask = pad_mels([x["target_mel"] for x in batch])
|
| 673 |
+
pitch_frame, _ = pad_mels([x["pitch_frame"] for x in batch])
|
| 674 |
+
target_wav = pad_wavs([x["target_wav"] for x in batch], [int(x["frame_count"]) for x in batch], hop_size)
|
| 675 |
+
return (
|
| 676 |
+
phone.to(device),
|
| 677 |
+
tone.to(device),
|
| 678 |
+
lang.to(device),
|
| 679 |
+
speakers.to(device),
|
| 680 |
+
duration.to(device),
|
| 681 |
+
energy.to(device),
|
| 682 |
+
bright.to(device),
|
| 683 |
+
token_pitch.to(device),
|
| 684 |
+
target_mel.to(device),
|
| 685 |
+
frame_mask.to(device),
|
| 686 |
+
pitch_frame.to(device),
|
| 687 |
+
target_wav.to(device),
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def masked_l1(pred: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 692 |
+
common = min(pred.shape[-1], target.shape[-1], mask.shape[-1])
|
| 693 |
+
pred = pred[..., :common]
|
| 694 |
+
target = target[..., :common]
|
| 695 |
+
mask = mask[:, :common].unsqueeze(1)
|
| 696 |
+
return (torch.abs(pred - target) * mask).sum() / (mask.sum() * pred.shape[1]).clamp_min(1.0)
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
def masked_mse(pred: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 700 |
+
common = min(pred.shape[-1], target.shape[-1], mask.shape[-1])
|
| 701 |
+
pred = pred[..., :common]
|
| 702 |
+
target = target[..., :common]
|
| 703 |
+
mask = mask[:, :common].unsqueeze(1)
|
| 704 |
+
return (((pred - target) ** 2) * mask).sum() / (mask.sum() * pred.shape[1]).clamp_min(1.0)
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
def weighted_frame_l1(pred: torch.Tensor, target: torch.Tensor, wmap: torch.Tensor, valid_count: torch.Tensor) -> torch.Tensor:
|
| 708 |
+
"""L1 with a PER-FRAME weight map wmap [B,T] (0 in pad). Normalized by valid_count*n_mels so
|
| 709 |
+
that wmap == frame_mask reproduces masked_l1 exactly. Used for per-language mel weighting (rank 4)."""
|
| 710 |
+
common = min(pred.shape[-1], target.shape[-1], wmap.shape[-1])
|
| 711 |
+
p = pred[..., :common]; t = target[..., :common]; w = wmap[:, :common].unsqueeze(1)
|
| 712 |
+
return (torch.abs(p - t) * w).sum() / (valid_count * pred.shape[1]).clamp_min(1.0)
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def weighted_frame_mse(pred: torch.Tensor, target: torch.Tensor, wmap: torch.Tensor, valid_count: torch.Tensor) -> torch.Tensor:
|
| 716 |
+
common = min(pred.shape[-1], target.shape[-1], wmap.shape[-1])
|
| 717 |
+
p = pred[..., :common]; t = target[..., :common]; w = wmap[:, :common].unsqueeze(1)
|
| 718 |
+
return (((p - t) ** 2) * w).sum() / (valid_count * pred.shape[1]).clamp_min(1.0)
|
| 719 |
+
|
| 720 |
+
|
| 721 |
+
def masked_delta_loss(pred: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 722 |
+
common = min(pred.shape[-1], target.shape[-1], mask.shape[-1])
|
| 723 |
+
if common < 2:
|
| 724 |
+
return torch.zeros((), device=pred.device)
|
| 725 |
+
dp = pred[..., 1:common] - pred[..., : common - 1]
|
| 726 |
+
dt = target[..., 1:common] - target[..., : common - 1]
|
| 727 |
+
dm = (mask[:, 1:common] & mask[:, : common - 1]).unsqueeze(1)
|
| 728 |
+
return (torch.abs(dp - dt) * dm).sum() / (dm.sum() * pred.shape[1]).clamp_min(1.0)
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
def masked_accel_loss(pred: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 732 |
+
common = min(pred.shape[-1], target.shape[-1], mask.shape[-1])
|
| 733 |
+
if common < 3:
|
| 734 |
+
return torch.zeros((), device=pred.device)
|
| 735 |
+
dp = pred[..., 2:common] - 2.0 * pred[..., 1 : common - 1] + pred[..., : common - 2]
|
| 736 |
+
dt = target[..., 2:common] - 2.0 * target[..., 1 : common - 1] + target[..., : common - 2]
|
| 737 |
+
dm = (mask[:, 2:common] & mask[:, 1 : common - 1] & mask[:, : common - 2]).unsqueeze(1)
|
| 738 |
+
return (torch.abs(dp - dt) * dm).sum() / (dm.sum() * pred.shape[1]).clamp_min(1.0)
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
def token_mse(pred: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 742 |
+
common = min(pred.shape[1], target.shape[1], mask.shape[1])
|
| 743 |
+
pred = pred[:, :common]
|
| 744 |
+
target = target[:, :common]
|
| 745 |
+
mask = mask[:, :common]
|
| 746 |
+
return (((pred - target) ** 2) * mask).sum() / mask.sum().clamp_min(1.0)
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
def token_mse_nd(pred: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 750 |
+
common = min(pred.shape[1], target.shape[1], mask.shape[1])
|
| 751 |
+
pred = pred[:, :common]
|
| 752 |
+
target = target[:, :common]
|
| 753 |
+
mask = mask[:, :common].unsqueeze(-1)
|
| 754 |
+
return (((pred - target) ** 2) * mask).sum() / (mask.sum() * pred.shape[-1]).clamp_min(1.0)
|
| 755 |
+
|
| 756 |
+
|
| 757 |
+
def group_duration_targets(phone: torch.Tensor, durations: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 758 |
+
grouped = torch.zeros_like(durations, dtype=torch.float32)
|
| 759 |
+
mask = torch.zeros_like(phone, dtype=torch.bool)
|
| 760 |
+
for batch_index in range(phone.shape[0]):
|
| 761 |
+
pending: list[torch.Tensor] = []
|
| 762 |
+
last_visible: int | None = None
|
| 763 |
+
for token_index in range(phone.shape[1]):
|
| 764 |
+
pending.append(durations[batch_index, token_index].float())
|
| 765 |
+
if int(phone[batch_index, token_index].item()) != 0:
|
| 766 |
+
grouped[batch_index, token_index] = torch.log1p(torch.stack(pending).sum())
|
| 767 |
+
mask[batch_index, token_index] = True
|
| 768 |
+
pending = []
|
| 769 |
+
last_visible = token_index
|
| 770 |
+
if pending and last_visible is not None:
|
| 771 |
+
value = torch.expm1(grouped[batch_index, last_visible]) + torch.stack(pending).sum()
|
| 772 |
+
grouped[batch_index, last_visible] = torch.log1p(value)
|
| 773 |
+
return grouped, mask
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
def masked_wav_l1(pred: torch.Tensor, target: torch.Tensor, frame_mask: torch.Tensor, hop_size: int) -> torch.Tensor:
|
| 777 |
+
if pred.dim() == 3:
|
| 778 |
+
pred = pred.squeeze(1)
|
| 779 |
+
common = min(pred.shape[-1], target.shape[-1], frame_mask.shape[-1] * hop_size)
|
| 780 |
+
pred = pred[:, :common]
|
| 781 |
+
target = target[:, :common]
|
| 782 |
+
sample_mask = frame_mask.repeat_interleave(hop_size, dim=1)[:, :common].to(pred.dtype)
|
| 783 |
+
return (torch.abs(pred - target) * sample_mask).sum() / sample_mask.sum().clamp_min(1.0)
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
def load_frozen_vocoder(path: Path, device: torch.device) -> tuple[HifiGanGenerator, HifiGanConfig]:
|
| 787 |
+
ckpt = torch.load(path, map_location=device, weights_only=False)
|
| 788 |
+
cfg_payload = ckpt.get("config") or {"variant": "v2plus"}
|
| 789 |
+
cfg = HifiGanConfig(**cfg_payload) if isinstance(cfg_payload, dict) else cfg_payload
|
| 790 |
+
vocoder = HifiGanGenerator(cfg).to(device)
|
| 791 |
+
vocoder.load_state_dict(ckpt["generator"])
|
| 792 |
+
vocoder.eval()
|
| 793 |
+
for param in vocoder.parameters():
|
| 794 |
+
param.requires_grad_(False)
|
| 795 |
+
return vocoder, cfg
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def load_model_state_flexible(model: nn.Module, state: dict[str, torch.Tensor]) -> tuple[int, int]:
|
| 799 |
+
current = model.state_dict()
|
| 800 |
+
compatible = {key: value for key, value in state.items() if key in current and current[key].shape == value.shape}
|
| 801 |
+
model.load_state_dict(compatible, strict=False)
|
| 802 |
+
return len(compatible), len(state) - len(compatible)
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
def set_trainable_by_mode(model: MicroFastSpeech, mode: str) -> None:
|
| 806 |
+
if mode == "all":
|
| 807 |
+
for param in model.parameters():
|
| 808 |
+
param.requires_grad_(True)
|
| 809 |
+
return
|
| 810 |
+
for param in model.parameters():
|
| 811 |
+
param.requires_grad_(False)
|
| 812 |
+
prefixes: tuple[str, ...]
|
| 813 |
+
if mode == "duration":
|
| 814 |
+
prefixes = ("phone.", "tone.", "lang.", "speaker.", "speaker_proj.", "encoder.", "duration_head.")
|
| 815 |
+
elif mode == "predictors":
|
| 816 |
+
prefixes = (
|
| 817 |
+
"phone.",
|
| 818 |
+
"tone.",
|
| 819 |
+
"lang.",
|
| 820 |
+
"speaker.",
|
| 821 |
+
"speaker_proj.",
|
| 822 |
+
"encoder.",
|
| 823 |
+
"duration_head.",
|
| 824 |
+
"energy_head.",
|
| 825 |
+
"bright_head.",
|
| 826 |
+
"pitch_head.",
|
| 827 |
+
)
|
| 828 |
+
elif mode == "heads":
|
| 829 |
+
prefixes = (
|
| 830 |
+
"duration_head.",
|
| 831 |
+
"energy_head.",
|
| 832 |
+
"bright_head.",
|
| 833 |
+
"pitch_head.",
|
| 834 |
+
"predictor_context.",
|
| 835 |
+
"duration_delta.",
|
| 836 |
+
"energy_delta.",
|
| 837 |
+
"bright_delta.",
|
| 838 |
+
"pitch_delta.",
|
| 839 |
+
)
|
| 840 |
+
elif mode == "contextual":
|
| 841 |
+
prefixes = (
|
| 842 |
+
"predictor_context.",
|
| 843 |
+
"duration_delta.",
|
| 844 |
+
"energy_delta.",
|
| 845 |
+
"bright_delta.",
|
| 846 |
+
"pitch_delta.",
|
| 847 |
+
)
|
| 848 |
+
elif mode == "group_duration":
|
| 849 |
+
prefixes = ("group_duration_delta.",)
|
| 850 |
+
elif mode == "decoder_adapt":
|
| 851 |
+
prefixes = (
|
| 852 |
+
"energy_proj.",
|
| 853 |
+
"bright_proj.",
|
| 854 |
+
"pitch_proj.",
|
| 855 |
+
"abs_frame.",
|
| 856 |
+
"frame_proj.",
|
| 857 |
+
"local_ctx.",
|
| 858 |
+
"decoder.",
|
| 859 |
+
"frame_gru.",
|
| 860 |
+
"mel_head.",
|
| 861 |
+
"postnet.",
|
| 862 |
+
)
|
| 863 |
+
else:
|
| 864 |
+
raise ValueError(f"Unknown trainable mode: {mode}")
|
| 865 |
+
for name, param in model.named_parameters():
|
| 866 |
+
if name.startswith(prefixes):
|
| 867 |
+
param.requires_grad_(True)
|
| 868 |
+
|
| 869 |
+
|
| 870 |
+
def latest_checkpoint(out_dir: Path) -> Path | None:
|
| 871 |
+
found = []
|
| 872 |
+
for path in out_dir.glob("inflect-micro-fastspeech-*.pt"):
|
| 873 |
+
tail = path.stem.rsplit("-", 1)[-1]
|
| 874 |
+
if tail.isdigit():
|
| 875 |
+
found.append((int(tail), path))
|
| 876 |
+
return max(found)[1] if found else None
|
| 877 |
+
|
| 878 |
+
|
| 879 |
+
def save_checkpoint(path: Path, model: nn.Module, optim, cfg: MicroFastSpeechConfig, step: int, args, speakers: dict[str, int]) -> None:
|
| 880 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 881 |
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
| 882 |
+
torch.save(
|
| 883 |
+
{
|
| 884 |
+
"model": model.state_dict(),
|
| 885 |
+
"optim": optim.state_dict(),
|
| 886 |
+
"config": asdict(cfg),
|
| 887 |
+
"step": step,
|
| 888 |
+
"speakers": speakers,
|
| 889 |
+
"args": {k: str(v) if isinstance(v, Path) else v for k, v in vars(args).items()},
|
| 890 |
+
"params": count_parameters(model),
|
| 891 |
+
},
|
| 892 |
+
tmp,
|
| 893 |
+
)
|
| 894 |
+
tmp.replace(path)
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
class MelDiscriminator(nn.Module):
|
| 898 |
+
"""Training-only multi-conv mel discriminator (LSGAN). Discarded at inference, so the
|
| 899 |
+
exported ONNX graph is byte-for-byte unchanged. Adversarial + feature-matching loss push
|
| 900 |
+
the predicted mel onto the real-mel manifold, countering the L1/L2 regression-to-the-mean
|
| 901 |
+
over-smoothing floor (Ren et al. ACL 2022; GANSpeech IS2021)."""
|
| 902 |
+
|
| 903 |
+
def __init__(self, n_mels: int, ch: int = 64) -> None:
|
| 904 |
+
super().__init__()
|
| 905 |
+
from torch.nn.utils import weight_norm as wn
|
| 906 |
+
self.convs = nn.ModuleList([
|
| 907 |
+
wn(nn.Conv1d(n_mels, ch, 5, 1, 2)),
|
| 908 |
+
wn(nn.Conv1d(ch, ch, 5, 2, 2)),
|
| 909 |
+
wn(nn.Conv1d(ch, ch * 2, 5, 2, 2)),
|
| 910 |
+
wn(nn.Conv1d(ch * 2, ch * 2, 5, 2, 2)),
|
| 911 |
+
])
|
| 912 |
+
self.post = wn(nn.Conv1d(ch * 2, 1, 3, 1, 1))
|
| 913 |
+
|
| 914 |
+
def forward(self, mel: torch.Tensor): # mel [B, n_mels, T]
|
| 915 |
+
fmaps = []
|
| 916 |
+
x = mel
|
| 917 |
+
for c in self.convs:
|
| 918 |
+
x = F.leaky_relu(c(x), 0.1)
|
| 919 |
+
fmaps.append(x)
|
| 920 |
+
return self.post(x), fmaps
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
class MelDiscriminator2D(nn.Module):
|
| 924 |
+
"""RANK 5: training-only 2D time-frequency mel discriminator (spectral-norm). Treats the mel as
|
| 925 |
+
an image [B,1,n_mels,T] so it judges JOINT time-frequency texture (formant structure), not just
|
| 926 |
+
per-frame spectra like the Conv1d disc that failed in M11. Discarded at inference -> ONNX unchanged."""
|
| 927 |
+
|
| 928 |
+
def __init__(self, n_mels: int = 80) -> None:
|
| 929 |
+
super().__init__()
|
| 930 |
+
from torch.nn.utils import spectral_norm as sn
|
| 931 |
+
chs = [1, 32, 64, 128, 256, 256]
|
| 932 |
+
self.convs = nn.ModuleList([
|
| 933 |
+
sn(nn.Conv2d(chs[i], chs[i + 1], 5, 2, 2)) for i in range(5)
|
| 934 |
+
])
|
| 935 |
+
self.post = sn(nn.Conv2d(256, 1, 3, 1, 1))
|
| 936 |
+
|
| 937 |
+
def forward(self, mel: torch.Tensor): # mel [B, n_mels, T] -> image [B,1,n_mels,T]
|
| 938 |
+
x = mel.unsqueeze(1)
|
| 939 |
+
fmaps = []
|
| 940 |
+
for c in self.convs:
|
| 941 |
+
x = F.leaky_relu(c(x), 0.2)
|
| 942 |
+
fmaps.append(x)
|
| 943 |
+
return self.post(x), fmaps
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
def train(args: argparse.Namespace) -> None:
|
| 947 |
+
device = torch.device(args.device)
|
| 948 |
+
rows = load_rows(args.durations_jsonl, args.max_rows)
|
| 949 |
+
speakers = {voice: idx for idx, voice in enumerate(sorted({str(r.get("voice_id") or "mark") for r in rows}))}
|
| 950 |
+
max_phone_id = max(max(map(int, r["phone_ids"])) for r in rows)
|
| 951 |
+
max_tone_id = max(max(map(int, r["tone_ids"])) for r in rows)
|
| 952 |
+
max_lang_id = max(max(map(int, r["lang_ids"])) for r in rows)
|
| 953 |
+
cfg = MicroFastSpeechConfig(
|
| 954 |
+
vocab_size=max(256, max_phone_id + 1),
|
| 955 |
+
tone_size=max(16, max_tone_id + 1),
|
| 956 |
+
lang_size=max(4, max_lang_id + 1),
|
| 957 |
+
speaker_count=max(2, len(speakers)),
|
| 958 |
+
hidden=args.hidden,
|
| 959 |
+
encoder_layers=args.encoder_layers,
|
| 960 |
+
decoder_layers=args.decoder_layers,
|
| 961 |
+
decoder_ff_mult=args.decoder_ff_mult,
|
| 962 |
+
max_frames=args.max_frames,
|
| 963 |
+
postnet_scale=args.postnet_scale,
|
| 964 |
+
abs_frame_bins=args.abs_frame_bins,
|
| 965 |
+
use_contextual_predictors=args.contextual_predictors,
|
| 966 |
+
use_group_duration_planner=args.group_duration_planner,
|
| 967 |
+
sample_rate=args.sample_rate,
|
| 968 |
+
n_mels=make_config(args.vocoder_variant).num_mels, # auto-match acoustic mel count to vocoder variant
|
| 969 |
+
)
|
| 970 |
+
for row in rows:
|
| 971 |
+
row["speaker_id"] = speakers[str(row.get("voice_id") or "mark")]
|
| 972 |
+
random.Random(args.seed).shuffle(rows)
|
| 973 |
+
|
| 974 |
+
model = MicroFastSpeech(cfg).to(device)
|
| 975 |
+
start_step = 0
|
| 976 |
+
if args.init_checkpoint and not args.resume:
|
| 977 |
+
ckpt = torch.load(args.init_checkpoint, map_location=device, weights_only=False)
|
| 978 |
+
copied, skipped = load_model_state_flexible(model, ckpt["model"])
|
| 979 |
+
print(f"Initialized model from {args.init_checkpoint} ({copied} tensors copied, {skipped} skipped)")
|
| 980 |
+
set_trainable_by_mode(model, args.trainable)
|
| 981 |
+
trainable_params = [param for param in model.parameters() if param.requires_grad]
|
| 982 |
+
optim = torch.optim.AdamW(trainable_params, lr=args.lr, betas=(0.9, 0.98), weight_decay=args.weight_decay)
|
| 983 |
+
mel_disc = None
|
| 984 |
+
disc_optim = None
|
| 985 |
+
if getattr(args, "mel_gan_weight", 0.0) > 0.0 or getattr(args, "mel_fm_weight", 0.0) > 0.0:
|
| 986 |
+
if getattr(args, "gan_2d", False):
|
| 987 |
+
mel_disc = MelDiscriminator2D(cfg.n_mels).to(device)
|
| 988 |
+
else:
|
| 989 |
+
mel_disc = MelDiscriminator(cfg.n_mels).to(device)
|
| 990 |
+
disc_optim = torch.optim.AdamW(mel_disc.parameters(), lr=args.disc_lr, betas=(0.5, 0.9))
|
| 991 |
+
print(f"Mel-GAN ON ({'2D' if getattr(args,'gan_2d',False) else '1D'}): adv={args.mel_gan_weight} "
|
| 992 |
+
f"fm={'auto' if getattr(args,'gan_fm_auto',False) else args.mel_fm_weight} warmup={args.gan_warmup_steps} "
|
| 993 |
+
f"r1={getattr(args,'gan_r1_gamma',0.0)} crop={getattr(args,'gan_crop',0)} "
|
| 994 |
+
f"disc_lr={args.disc_lr} disc_params={sum(p.numel() for p in mel_disc.parameters()):,}", flush=True)
|
| 995 |
+
if args.resume:
|
| 996 |
+
ckpt_path = latest_checkpoint(args.out_dir)
|
| 997 |
+
if ckpt_path:
|
| 998 |
+
ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
|
| 999 |
+
model.load_state_dict(ckpt["model"])
|
| 1000 |
+
optim.load_state_dict(ckpt["optim"])
|
| 1001 |
+
start_step = int(ckpt.get("step") or 0)
|
| 1002 |
+
print(f"Resumed {ckpt_path} at step {start_step}")
|
| 1003 |
+
|
| 1004 |
+
hifi_cfg = make_config(args.vocoder_variant)
|
| 1005 |
+
mel_frontend = MelFrontend(hifi_cfg).to(device)
|
| 1006 |
+
assert hifi_cfg.sample_rate == cfg.sample_rate, f"vocoder sr {hifi_cfg.sample_rate} != cfg sr {cfg.sample_rate}"
|
| 1007 |
+
prepared_rows = None
|
| 1008 |
+
if args.preload_features:
|
| 1009 |
+
print("Preloading audio/mel/pitch features...", flush=True)
|
| 1010 |
+
prepared_rows = [prepare_row_features(row, cfg, mel_frontend, device, args.max_seconds) for row in rows]
|
| 1011 |
+
total_frames = sum(int(row["frame_count"]) for row in prepared_rows)
|
| 1012 |
+
print(f"Preloaded {len(prepared_rows)} rows ({total_frames:,} frames)", flush=True)
|
| 1013 |
+
consistency_vocoder = None
|
| 1014 |
+
if args.vocoder_checkpoint:
|
| 1015 |
+
consistency_vocoder, consistency_cfg = load_frozen_vocoder(args.vocoder_checkpoint, device)
|
| 1016 |
+
if consistency_cfg.hop_size != hifi_cfg.hop_size:
|
| 1017 |
+
raise RuntimeError(f"Vocoder hop mismatch: {consistency_cfg.hop_size} != {hifi_cfg.hop_size}")
|
| 1018 |
+
print(f"Loaded frozen vocoder consistency checkpoint: {args.vocoder_checkpoint}")
|
| 1019 |
+
if (args.vocoder_wav_weight > 0.0 or args.vocoder_mel_weight > 0.0) and consistency_vocoder is None:
|
| 1020 |
+
raise RuntimeError("--vocoder-checkpoint is required when vocoder consistency losses are enabled")
|
| 1021 |
+
args.out_dir.mkdir(parents=True, exist_ok=True)
|
| 1022 |
+
(args.out_dir / "config.json").write_text(
|
| 1023 |
+
json.dumps({"config": asdict(cfg), "speakers": speakers, "rows": len(rows), "params": count_parameters(model)}, indent=2),
|
| 1024 |
+
encoding="utf-8",
|
| 1025 |
+
)
|
| 1026 |
+
|
| 1027 |
+
print(f"Rows: {len(rows)}")
|
| 1028 |
+
print(f"Speakers: {speakers}")
|
| 1029 |
+
print(f"Acoustic params: {count_parameters(model):,} ({count_parameters(model)/1_000_000:.3f}M)")
|
| 1030 |
+
print(f"Trainable params: {sum(p.numel() for p in model.parameters() if p.requires_grad):,} mode={args.trainable}")
|
| 1031 |
+
print(f"Total with V2+ vocoder: {(count_parameters(model)+1_426_842):,} ({(count_parameters(model)+1_426_842)/1_000_000:.3f}M)")
|
| 1032 |
+
|
| 1033 |
+
rng = random.Random(args.seed + start_step)
|
| 1034 |
+
# language-balanced sampling pool: replicate English-row indices by --en-upsample
|
| 1035 |
+
en_up = max(1, int(round(args.en_upsample)))
|
| 1036 |
+
sample_pool = list(range(len(rows)))
|
| 1037 |
+
if en_up > 1:
|
| 1038 |
+
en_idx = [i for i, r in enumerate(rows) if str(r.get("id") or "").startswith("en")]
|
| 1039 |
+
sample_pool = sample_pool + en_idx * (en_up - 1)
|
| 1040 |
+
print(f"lang-balance: {len(en_idx)} en rows upsampled x{en_up} -> pool {len(sample_pool)} "
|
| 1041 |
+
f"(en exposure ~{100*len(en_idx)*en_up/len(sample_pool):.0f}%)")
|
| 1042 |
+
step = start_step
|
| 1043 |
+
started = time.time()
|
| 1044 |
+
while step < args.steps:
|
| 1045 |
+
source_rows = prepared_rows if prepared_rows is not None else rows
|
| 1046 |
+
batch = [source_rows[sample_pool[rng.randrange(len(sample_pool))]] for _ in range(args.batch_size)]
|
| 1047 |
+
if prepared_rows is not None:
|
| 1048 |
+
phone, tone, lang, speaker, durations, energy_t, bright_t, pitch_token_t, target_mel, frame_mask, pitch_frame, target_wav = collate_prepared(
|
| 1049 |
+
batch, device, hifi_cfg.hop_size
|
| 1050 |
+
)
|
| 1051 |
+
else:
|
| 1052 |
+
phone, tone, lang, speaker, durations, energy_t, bright_t, pitch_token_t, target_mel, frame_mask, pitch_frame, target_wav = collate(
|
| 1053 |
+
batch, cfg, mel_frontend, device, args.max_seconds, hifi_cfg.hop_size
|
| 1054 |
+
)
|
| 1055 |
+
out = model(phone, tone, lang, speaker, durations, energy_t, bright_t, pitch_frame)
|
| 1056 |
+
token_mask = out["token_mask"]
|
| 1057 |
+
log_dur_t = torch.log1p(durations.float())
|
| 1058 |
+
group_log_dur_t, group_mask = group_duration_targets(phone, durations)
|
| 1059 |
+
mel_l1 = masked_l1(out["mel"], target_mel, frame_mask)
|
| 1060 |
+
mel_mse = masked_mse(out["mel"], target_mel, frame_mask)
|
| 1061 |
+
# RANK 4: per-language mel-loss weighting. Down-weight English frames so they stop
|
| 1062 |
+
# crowding out Chinese in the 4.6M budget. At 1:1 reproduces M7 exactly (gated off).
|
| 1063 |
+
_lw_zh = getattr(args, "lang_loss_zh_weight", 1.0)
|
| 1064 |
+
_lw_en = getattr(args, "lang_loss_en_weight", 1.0)
|
| 1065 |
+
if _lw_zh != 1.0 or _lw_en != 1.0:
|
| 1066 |
+
T = out["mel"].shape[-1]
|
| 1067 |
+
lang_frame = model.expand_token_feature(lang.unsqueeze(-1).float(), durations).squeeze(-1) # [B,Tf]
|
| 1068 |
+
common = min(T, lang_frame.shape[-1], frame_mask.shape[-1])
|
| 1069 |
+
fm = frame_mask[:, :common].to(out["mel"].dtype)
|
| 1070 |
+
lf = lang_frame[:, :common]
|
| 1071 |
+
wmap = fm * torch.where(lf < 0.5, float(_lw_zh), float(_lw_en)) # lang 0 = zh
|
| 1072 |
+
vc = frame_mask[:, :common].sum()
|
| 1073 |
+
mel_l1 = weighted_frame_l1(out["mel"], target_mel, wmap, vc)
|
| 1074 |
+
mel_mse = weighted_frame_mse(out["mel"], target_mel, wmap, vc)
|
| 1075 |
+
delta = masked_delta_loss(out["mel"], target_mel, frame_mask)
|
| 1076 |
+
accel = masked_accel_loss(out["mel"], target_mel, frame_mask)
|
| 1077 |
+
dur_loss = token_mse(out["log_dur"], log_dur_t, token_mask)
|
| 1078 |
+
group_dur_loss = token_mse(out["group_log_dur"], group_log_dur_t, group_mask)
|
| 1079 |
+
energy_loss = token_mse(out["energy"], energy_t, token_mask)
|
| 1080 |
+
bright_loss = token_mse(out["bright"], bright_t, token_mask)
|
| 1081 |
+
pitch_loss = token_mse_nd(out["pitch"], pitch_token_t, token_mask)
|
| 1082 |
+
predicted_prosody_mel_loss = torch.zeros((), device=device)
|
| 1083 |
+
predicted_prosody_delta_loss = torch.zeros((), device=device)
|
| 1084 |
+
if args.predicted_prosody_mel_weight > 0.0 or args.predicted_prosody_delta_weight > 0.0:
|
| 1085 |
+
# Train the predictor heads against the acoustic result they produce at
|
| 1086 |
+
# inference, while retaining reference durations so this path remains
|
| 1087 |
+
# differentiable and isolates prosody exposure bias.
|
| 1088 |
+
predicted_conditioning = model(phone, tone, lang, speaker, durations)
|
| 1089 |
+
if args.predicted_prosody_mel_weight > 0.0:
|
| 1090 |
+
predicted_prosody_mel_loss = masked_l1(predicted_conditioning["mel"], target_mel, frame_mask)
|
| 1091 |
+
if args.predicted_prosody_delta_weight > 0.0:
|
| 1092 |
+
predicted_prosody_delta_loss = masked_delta_loss(predicted_conditioning["mel"], target_mel, frame_mask)
|
| 1093 |
+
robust_prosody_mel_loss = torch.zeros((), device=device)
|
| 1094 |
+
robust_prosody_delta_loss = torch.zeros((), device=device)
|
| 1095 |
+
if args.robust_prosody_mel_weight > 0.0 or args.robust_prosody_delta_weight > 0.0:
|
| 1096 |
+
robust_conditioning = model(
|
| 1097 |
+
phone,
|
| 1098 |
+
tone,
|
| 1099 |
+
lang,
|
| 1100 |
+
speaker,
|
| 1101 |
+
durations,
|
| 1102 |
+
energy_t,
|
| 1103 |
+
bright_t,
|
| 1104 |
+
pitch_frame,
|
| 1105 |
+
predicted_prosody_mix=args.robust_prosody_mix,
|
| 1106 |
+
detach_mixed_predictions=True,
|
| 1107 |
+
)
|
| 1108 |
+
if args.robust_prosody_mel_weight > 0.0:
|
| 1109 |
+
robust_prosody_mel_loss = masked_l1(robust_conditioning["mel"], target_mel, frame_mask)
|
| 1110 |
+
if args.robust_prosody_delta_weight > 0.0:
|
| 1111 |
+
robust_prosody_delta_loss = masked_delta_loss(robust_conditioning["mel"], target_mel, frame_mask)
|
| 1112 |
+
voc_wav_loss = torch.zeros((), device=device)
|
| 1113 |
+
voc_mel_loss = torch.zeros((), device=device)
|
| 1114 |
+
voc_mrstft_loss = torch.zeros((), device=device)
|
| 1115 |
+
_mrstft_w = getattr(args, "vocoder_mrstft_weight", 0.0)
|
| 1116 |
+
if consistency_vocoder is not None and (args.vocoder_wav_weight > 0.0 or args.vocoder_mel_weight > 0.0 or _mrstft_w > 0.0):
|
| 1117 |
+
pred_wav = consistency_vocoder(out["mel"].clamp(-12.0, 2.0))
|
| 1118 |
+
if args.vocoder_wav_weight > 0.0:
|
| 1119 |
+
voc_wav_loss = masked_wav_l1(pred_wav, target_wav, frame_mask, hifi_cfg.hop_size)
|
| 1120 |
+
if args.vocoder_mel_weight > 0.0:
|
| 1121 |
+
pred_recon_mel = mel_frontend(pred_wav.squeeze(1))
|
| 1122 |
+
voc_mel_loss = masked_l1(pred_recon_mel, target_mel, frame_mask)
|
| 1123 |
+
if _mrstft_w > 0.0:
|
| 1124 |
+
# RANK 3: multi-resolution STFT magnitude loss through the frozen vocoder.
|
| 1125 |
+
# 8kHz-appropriate FFT sizes (<=512; >512 over-resolves a 4kHz-Nyquist signal).
|
| 1126 |
+
from .vocoder import stft_mag_loss
|
| 1127 |
+
_hop = hifi_cfg.hop_size
|
| 1128 |
+
_common = min(pred_wav.shape[-1], target_wav.shape[-1], frame_mask.shape[-1] * _hop)
|
| 1129 |
+
_sm = frame_mask.repeat_interleave(_hop, dim=1)[:, :_common].to(pred_wav.dtype)
|
| 1130 |
+
_pw = pred_wav.squeeze(1)[:, :_common] * _sm
|
| 1131 |
+
_tw = (target_wav.squeeze(1) if target_wav.dim() == 3 else target_wav)[:, :_common] * _sm
|
| 1132 |
+
voc_mrstft_loss = stft_mag_loss(_pw, _tw, (128, 256, 512), (32, 64, 128), (128, 256, 512))
|
| 1133 |
+
# ramp the MR-STFT weight in from --mrstft-warmup-steps over 4000 steps (limit early vocoder-quirk exploitation)
|
| 1134 |
+
mrstft_eff = _mrstft_w * min(1.0, max(0.0, (step - args.mrstft_warmup_steps) / 4000.0)) if _mrstft_w > 0.0 else 0.0
|
| 1135 |
+
loss = (
|
| 1136 |
+
mel_l1
|
| 1137 |
+
+ args.mse_weight * mel_mse
|
| 1138 |
+
+ args.delta_weight * delta
|
| 1139 |
+
+ args.accel_weight * accel
|
| 1140 |
+
+ args.duration_weight * dur_loss
|
| 1141 |
+
+ args.group_duration_weight * group_dur_loss
|
| 1142 |
+
+ args.energy_weight * energy_loss
|
| 1143 |
+
+ args.bright_weight * bright_loss
|
| 1144 |
+
+ args.pitch_weight * pitch_loss
|
| 1145 |
+
+ args.predicted_prosody_mel_weight * predicted_prosody_mel_loss
|
| 1146 |
+
+ args.predicted_prosody_delta_weight * predicted_prosody_delta_loss
|
| 1147 |
+
+ args.robust_prosody_mel_weight * robust_prosody_mel_loss
|
| 1148 |
+
+ args.robust_prosody_delta_weight * robust_prosody_delta_loss
|
| 1149 |
+
+ args.vocoder_wav_weight * voc_wav_loss
|
| 1150 |
+
+ args.vocoder_mel_weight * voc_mel_loss
|
| 1151 |
+
+ mrstft_eff * voc_mrstft_loss
|
| 1152 |
+
)
|
| 1153 |
+
gan_g = torch.zeros((), device=device)
|
| 1154 |
+
gan_fm = torch.zeros((), device=device)
|
| 1155 |
+
gan_d = torch.zeros((), device=device)
|
| 1156 |
+
if mel_disc is not None and step >= args.gan_warmup_steps:
|
| 1157 |
+
T = out["mel"].shape[-1]
|
| 1158 |
+
m = frame_mask[:, :T].unsqueeze(1).to(out["mel"].dtype)
|
| 1159 |
+
real = target_mel[..., :T] * m
|
| 1160 |
+
fake = out["mel"] * m
|
| 1161 |
+
# RANK 5: optional random time-crop (2D disc judges local TF texture; stabilizes + speeds).
|
| 1162 |
+
_crop = getattr(args, "gan_crop", 0)
|
| 1163 |
+
if _crop > 0 and T > _crop:
|
| 1164 |
+
_s = int(torch.randint(0, T - _crop + 1, (1,)).item())
|
| 1165 |
+
real = real[..., _s:_s + _crop]; fake = fake[..., _s:_s + _crop]
|
| 1166 |
+
# Discriminator step (LSGAN) with optional lazy R1 on real mels every 16 steps.
|
| 1167 |
+
_r1 = getattr(args, "gan_r1_gamma", 0.0)
|
| 1168 |
+
do_r1 = _r1 > 0.0 and (step % 16 == 0)
|
| 1169 |
+
if do_r1:
|
| 1170 |
+
real = real.detach().requires_grad_(True)
|
| 1171 |
+
d_real, _ = mel_disc(real)
|
| 1172 |
+
d_fake_d, _ = mel_disc(fake.detach())
|
| 1173 |
+
gan_d = 0.5 * ((d_real - 1.0) ** 2).mean() + 0.5 * (d_fake_d ** 2).mean()
|
| 1174 |
+
if do_r1:
|
| 1175 |
+
gp = torch.autograd.grad(d_real.sum(), real, create_graph=True)[0]
|
| 1176 |
+
gan_d = gan_d + (_r1 / 2.0) * gp.pow(2).flatten(1).sum(1).mean()
|
| 1177 |
+
disc_optim.zero_grad(set_to_none=True)
|
| 1178 |
+
gan_d.backward()
|
| 1179 |
+
torch.nn.utils.clip_grad_norm_(mel_disc.parameters(), args.grad_clip)
|
| 1180 |
+
disc_optim.step()
|
| 1181 |
+
# Generator adversarial + feature-matching (real features detached).
|
| 1182 |
+
d_fake_g, feats_fake = mel_disc(fake)
|
| 1183 |
+
_, feats_real = mel_disc(real.detach())
|
| 1184 |
+
gan_g = ((d_fake_g - 1.0) ** 2).mean()
|
| 1185 |
+
gan_fm = sum(F.l1_loss(ff, fr.detach()) for ff, fr in zip(feats_fake, feats_real)) / len(feats_fake)
|
| 1186 |
+
# auto-FM scaling: lambda_FM = (recon / FM).detach().clamp[0,50] (GANSpeech-style; FM does the work)
|
| 1187 |
+
if getattr(args, "gan_fm_auto", False):
|
| 1188 |
+
fm_w = (mel_l1.detach() / (gan_fm.detach() + 1e-8)).clamp(0.0, 50.0)
|
| 1189 |
+
else:
|
| 1190 |
+
fm_w = args.mel_fm_weight
|
| 1191 |
+
loss = loss + args.mel_gan_weight * gan_g + fm_w * gan_fm
|
| 1192 |
+
optim.zero_grad(set_to_none=True)
|
| 1193 |
+
loss.backward()
|
| 1194 |
+
grad = torch.nn.utils.clip_grad_norm_(trainable_params, args.grad_clip)
|
| 1195 |
+
optim.step()
|
| 1196 |
+
step += 1
|
| 1197 |
+
|
| 1198 |
+
if step == 1 or step % args.log_interval == 0:
|
| 1199 |
+
elapsed = max(1e-6, time.time() - started)
|
| 1200 |
+
speed = (step - start_step) / elapsed
|
| 1201 |
+
eta = (args.steps - step) / max(1e-6, speed)
|
| 1202 |
+
print(
|
| 1203 |
+
f"step={step}/{args.steps} loss={loss.item():.4f} mel={mel_l1.item():.4f} "
|
| 1204 |
+
f"mse={mel_mse.item():.4f} delta={delta.item():.4f} accel={accel.item():.4f} dur={dur_loss.item():.4f} "
|
| 1205 |
+
f"gdur={group_dur_loss.item():.4f} "
|
| 1206 |
+
f"energy={energy_loss.item():.4f} bright={bright_loss.item():.4f} "
|
| 1207 |
+
f"pitch={pitch_loss.item():.4f} pmel={predicted_prosody_mel_loss.item():.4f} "
|
| 1208 |
+
f"pdelta={predicted_prosody_delta_loss.item():.4f} rmel={robust_prosody_mel_loss.item():.4f} "
|
| 1209 |
+
f"rdelta={robust_prosody_delta_loss.item():.4f} vwav={voc_wav_loss.item():.4f} "
|
| 1210 |
+
f"vmel={voc_mel_loss.item():.4f} mrstft={voc_mrstft_loss.item():.4f} ganG={gan_g.item():.4f} ganFM={gan_fm.item():.4f} ganD={gan_d.item():.4f} grad={float(grad):.2f} "
|
| 1211 |
+
f"speed={speed:.3f} step/s eta={eta/60:.1f}m",
|
| 1212 |
+
flush=True,
|
| 1213 |
+
)
|
| 1214 |
+
if step % args.save_interval == 0 or step >= args.steps:
|
| 1215 |
+
save_checkpoint(args.out_dir / f"inflect-micro-fastspeech-{step}.pt", model, optim, cfg, step, args, speakers)
|
| 1216 |
+
save_checkpoint(args.out_dir / "inflect-micro-fastspeech-latest.pt", model, optim, cfg, step, args, speakers)
|
| 1217 |
+
|
| 1218 |
+
print(f"Done. {args.out_dir}")
|
| 1219 |
+
|
| 1220 |
+
|
| 1221 |
+
def main() -> None:
|
| 1222 |
+
ap = argparse.ArgumentParser(description="Train Inflect Micro duration-conditioned acoustic model.")
|
| 1223 |
+
ap.add_argument("--durations-jsonl", type=Path, required=True)
|
| 1224 |
+
ap.add_argument("--out-dir", type=Path, required=True)
|
| 1225 |
+
ap.add_argument("--vocoder-variant", type=str, default="v2plus")
|
| 1226 |
+
ap.add_argument("--sample-rate", type=int, default=24000)
|
| 1227 |
+
ap.add_argument("--en-upsample", type=float, default=1.0,
|
| 1228 |
+
help="Oversample English rows (id starts 'en') by this factor in the "
|
| 1229 |
+
"training sampler, to balance a zh-dominant bilingual corpus.")
|
| 1230 |
+
ap.add_argument("--max-rows", type=int, default=0)
|
| 1231 |
+
ap.add_argument("--steps", type=int, default=20000)
|
| 1232 |
+
ap.add_argument("--batch-size", type=int, default=6)
|
| 1233 |
+
ap.add_argument("--lr", type=float, default=2.0e-4)
|
| 1234 |
+
ap.add_argument("--weight-decay", type=float, default=1.0e-4)
|
| 1235 |
+
ap.add_argument("--hidden", type=int, default=168)
|
| 1236 |
+
ap.add_argument("--encoder-layers", type=int, default=5)
|
| 1237 |
+
ap.add_argument("--decoder-layers", type=int, default=6)
|
| 1238 |
+
ap.add_argument("--decoder-ff-mult", type=int, default=3)
|
| 1239 |
+
ap.add_argument("--max-seconds", type=float, default=12.0)
|
| 1240 |
+
ap.add_argument("--max-frames", type=int, default=1400)
|
| 1241 |
+
ap.add_argument("--mse-weight", type=float, default=0.25)
|
| 1242 |
+
ap.add_argument("--delta-weight", type=float, default=0.18)
|
| 1243 |
+
# Training-only mel GAN (anti over-smoothing). Discriminator discarded at inference -> ONNX unchanged.
|
| 1244 |
+
ap.add_argument("--mel-gan-weight", type=float, default=0.0, help="generator adversarial loss weight (0=off)")
|
| 1245 |
+
ap.add_argument("--mel-fm-weight", type=float, default=0.0, help="feature-matching loss weight")
|
| 1246 |
+
ap.add_argument("--disc-lr", type=float, default=2.0e-4, help="mel discriminator learning rate")
|
| 1247 |
+
ap.add_argument("--gan-warmup-steps", type=int, default=2000, help="steps of pure recon before GAN kicks in")
|
| 1248 |
+
# RANK 5: corrected GAN β 2D TF discriminator + auto-FM + R1 + crop
|
| 1249 |
+
ap.add_argument("--gan-2d", action="store_true", help="use 2D time-frequency mel discriminator (spectral-norm)")
|
| 1250 |
+
ap.add_argument("--gan-fm-auto", action="store_true", help="auto-scale feature-matching weight = (recon/FM).clamp[0,50]")
|
| 1251 |
+
ap.add_argument("--gan-r1-gamma", type=float, default=0.0, help="lazy R1 gradient-penalty gamma (every 16 steps)")
|
| 1252 |
+
ap.add_argument("--gan-crop", type=int, default=0, help="random time-crop width for the disc (0=off)")
|
| 1253 |
+
# RANK 3: multi-resolution STFT loss through the frozen consistency vocoder (anti over-smoothing, loss-only)
|
| 1254 |
+
ap.add_argument("--vocoder-mrstft-weight", type=float, default=0.0, help="MR-STFT-through-vocoder loss weight (0=off)")
|
| 1255 |
+
ap.add_argument("--mrstft-warmup-steps", type=int, default=4000, help="step at which MR-STFT ramp begins")
|
| 1256 |
+
# RANK 4: per-language mel-loss weighting (anti capacity-interference). 1:1 = M7 (gated off).
|
| 1257 |
+
ap.add_argument("--lang-loss-zh-weight", type=float, default=1.0, help="mel-loss weight on zh frames")
|
| 1258 |
+
ap.add_argument("--lang-loss-en-weight", type=float, default=1.0, help="mel-loss weight on en frames")
|
| 1259 |
+
ap.add_argument("--accel-weight", type=float, default=0.0)
|
| 1260 |
+
ap.add_argument("--duration-weight", type=float, default=0.08)
|
| 1261 |
+
ap.add_argument("--group-duration-weight", type=float, default=0.0)
|
| 1262 |
+
ap.add_argument("--energy-weight", type=float, default=0.04)
|
| 1263 |
+
ap.add_argument("--bright-weight", type=float, default=0.04)
|
| 1264 |
+
ap.add_argument("--pitch-weight", type=float, default=0.04)
|
| 1265 |
+
ap.add_argument("--predicted-prosody-mel-weight", type=float, default=0.0)
|
| 1266 |
+
ap.add_argument("--predicted-prosody-delta-weight", type=float, default=0.0)
|
| 1267 |
+
ap.add_argument("--robust-prosody-mix", type=float, default=0.0)
|
| 1268 |
+
ap.add_argument("--robust-prosody-mel-weight", type=float, default=0.0)
|
| 1269 |
+
ap.add_argument("--robust-prosody-delta-weight", type=float, default=0.0)
|
| 1270 |
+
ap.add_argument("--grad-clip", type=float, default=5.0)
|
| 1271 |
+
ap.add_argument("--postnet-scale", type=float, default=0.10)
|
| 1272 |
+
ap.add_argument("--abs-frame-bins", type=int, default=512)
|
| 1273 |
+
ap.add_argument("--init-checkpoint", type=Path)
|
| 1274 |
+
ap.add_argument("--vocoder-checkpoint", type=Path)
|
| 1275 |
+
ap.add_argument("--vocoder-wav-weight", type=float, default=0.0)
|
| 1276 |
+
ap.add_argument("--vocoder-mel-weight", type=float, default=0.0)
|
| 1277 |
+
ap.add_argument("--save-interval", type=int, default=2000)
|
| 1278 |
+
ap.add_argument("--log-interval", type=int, default=50)
|
| 1279 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 1280 |
+
ap.add_argument("--resume", action="store_true")
|
| 1281 |
+
ap.add_argument("--preload-features", action="store_true", help="Cache decoded audio, mels, pitch, and token features in RAM before training.")
|
| 1282 |
+
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 1283 |
+
ap.add_argument(
|
| 1284 |
+
"--trainable",
|
| 1285 |
+
choices=["all", "duration", "predictors", "heads", "contextual", "group_duration", "decoder_adapt"],
|
| 1286 |
+
default="all",
|
| 1287 |
+
)
|
| 1288 |
+
ap.add_argument("--contextual-predictors", action="store_true")
|
| 1289 |
+
ap.add_argument("--group-duration-planner", action="store_true")
|
| 1290 |
+
args = ap.parse_args()
|
| 1291 |
+
train(args)
|
| 1292 |
+
|
| 1293 |
+
|
| 1294 |
+
if __name__ == "__main__":
|
| 1295 |
+
main()
|
inflect_nano/text_cleaning.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
_QUOTE_TRANSLATION = str.maketrans(
|
| 7 |
+
{
|
| 8 |
+
"\u2018": "'",
|
| 9 |
+
"\u2019": "'",
|
| 10 |
+
"\u201c": "",
|
| 11 |
+
"\u201d": "",
|
| 12 |
+
"\u2014": ",",
|
| 13 |
+
"\u2013": ",",
|
| 14 |
+
";": ",",
|
| 15 |
+
":": ",",
|
| 16 |
+
"\n": ".",
|
| 17 |
+
}
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def clean_tinytts_text(text: str) -> str:
|
| 22 |
+
"""Normalize text into punctuation TinyTTS actually has symbols for."""
|
| 23 |
+
text = str(text).translate(_QUOTE_TRANSLATION)
|
| 24 |
+
text = text.replace("...", "β¦")
|
| 25 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 26 |
+
text = re.sub(r"\s+([,.!?β¦])", r"\1", text)
|
| 27 |
+
text = re.sub(r"([,.!?β¦]){2,}", r"\1", text)
|
| 28 |
+
return text
|
inflect_nano/vocoder.py
ADDED
|
@@ -0,0 +1,844 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import random
|
| 7 |
+
import time
|
| 8 |
+
from dataclasses import asdict, dataclass
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
import torchaudio
|
| 15 |
+
from torch.nn.utils import remove_weight_norm, spectral_norm, weight_norm
|
| 16 |
+
from torch.utils.data import DataLoader, Dataset
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass(frozen=True)
|
| 20 |
+
class HifiGanConfig:
|
| 21 |
+
variant: str
|
| 22 |
+
sample_rate: int = 24000
|
| 23 |
+
n_fft: int = 1024
|
| 24 |
+
hop_size: int = 256
|
| 25 |
+
win_size: int = 1024
|
| 26 |
+
num_mels: int = 80
|
| 27 |
+
fmin: float = 0.0
|
| 28 |
+
fmax: float = 12000.0
|
| 29 |
+
resblock: str = "1"
|
| 30 |
+
upsample_rates: tuple[int, ...] = (8, 8, 2, 2)
|
| 31 |
+
upsample_kernel_sizes: tuple[int, ...] = (16, 16, 4, 4)
|
| 32 |
+
upsample_initial_channel: int = 128
|
| 33 |
+
resblock_kernel_sizes: tuple[int, ...] = (3, 7, 11)
|
| 34 |
+
resblock_dilation_sizes: tuple[tuple[int, ...], ...] = ((1, 3, 5), (1, 3, 5), (1, 3, 5))
|
| 35 |
+
activation: str = "lrelu"
|
| 36 |
+
conditioning_channels: int = 0
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def make_config(variant: str) -> HifiGanConfig:
|
| 40 |
+
if variant == "v2":
|
| 41 |
+
return HifiGanConfig(variant="v2")
|
| 42 |
+
if variant == "v2plus":
|
| 43 |
+
return HifiGanConfig(variant="v2plus", upsample_initial_channel=160)
|
| 44 |
+
if variant == "v2wide":
|
| 45 |
+
return HifiGanConfig(variant="v2wide", upsample_initial_channel=176)
|
| 46 |
+
if variant == "snake_v2mid":
|
| 47 |
+
return HifiGanConfig(variant="snake_v2mid", upsample_initial_channel=144, activation="snake")
|
| 48 |
+
if variant == "snake_8k":
|
| 49 |
+
return HifiGanConfig(variant="snake_8k", sample_rate=8000, n_fft=512, hop_size=128,
|
| 50 |
+
win_size=512, num_mels=80, fmin=0.0, fmax=4000.0,
|
| 51 |
+
upsample_rates=(8, 4, 2, 2), upsample_kernel_sizes=(16, 8, 4, 4),
|
| 52 |
+
upsample_initial_channel=144, activation="snake")
|
| 53 |
+
if variant == "snake_8k40":
|
| 54 |
+
# 8kHz variant with 40 mels (vs 80): 0-4kHz over-resolved at 80 -> correlated channels
|
| 55 |
+
# encourage L1 mean-collapse; 40 mels is the natural count for a 4kHz band (anti over-smoothing).
|
| 56 |
+
return HifiGanConfig(variant="snake_8k40", sample_rate=8000, n_fft=512, hop_size=128,
|
| 57 |
+
win_size=512, num_mels=40, fmin=0.0, fmax=4000.0,
|
| 58 |
+
upsample_rates=(8, 4, 2, 2), upsample_kernel_sizes=(16, 8, 4, 4),
|
| 59 |
+
upsample_initial_channel=144, activation="snake")
|
| 60 |
+
if variant == "snake_v2balanced":
|
| 61 |
+
return HifiGanConfig(variant="snake_v2balanced", upsample_initial_channel=160, activation="snake")
|
| 62 |
+
if variant == "source_snake_v2balanced":
|
| 63 |
+
return HifiGanConfig(
|
| 64 |
+
variant="source_snake_v2balanced",
|
| 65 |
+
upsample_initial_channel=160,
|
| 66 |
+
activation="snake",
|
| 67 |
+
conditioning_channels=5,
|
| 68 |
+
)
|
| 69 |
+
if variant == "v3":
|
| 70 |
+
return HifiGanConfig(
|
| 71 |
+
variant="v3",
|
| 72 |
+
resblock="2",
|
| 73 |
+
upsample_rates=(8, 8, 4),
|
| 74 |
+
upsample_kernel_sizes=(16, 16, 8),
|
| 75 |
+
upsample_initial_channel=256,
|
| 76 |
+
resblock_kernel_sizes=(3, 5, 7),
|
| 77 |
+
resblock_dilation_sizes=((1, 2), (2, 6), (3, 12)),
|
| 78 |
+
)
|
| 79 |
+
raise ValueError(f"Unknown variant: {variant}")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def get_padding(kernel_size: int, dilation: int = 1) -> int:
|
| 83 |
+
return int((kernel_size * dilation - dilation) / 2)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class SnakeActivation(nn.Module):
|
| 87 |
+
def __init__(self, channels: int):
|
| 88 |
+
super().__init__()
|
| 89 |
+
self.log_alpha = nn.Parameter(torch.zeros(1, channels, 1))
|
| 90 |
+
|
| 91 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 92 |
+
alpha = self.log_alpha.exp().clamp(1e-4, 100.0)
|
| 93 |
+
return x + torch.sin(alpha * x).pow(2) / alpha
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def make_activation(channels: int, activation: str) -> nn.Module:
|
| 97 |
+
if activation == "snake":
|
| 98 |
+
return SnakeActivation(channels)
|
| 99 |
+
return nn.LeakyReLU(0.1)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class ResBlock1(nn.Module):
|
| 103 |
+
def __init__(self, channels: int, kernel_size: int, dilations: tuple[int, ...], activation: str = "lrelu"):
|
| 104 |
+
super().__init__()
|
| 105 |
+
self.convs1 = nn.ModuleList(
|
| 106 |
+
[
|
| 107 |
+
weight_norm(
|
| 108 |
+
nn.Conv1d(
|
| 109 |
+
channels,
|
| 110 |
+
channels,
|
| 111 |
+
kernel_size,
|
| 112 |
+
1,
|
| 113 |
+
dilation=d,
|
| 114 |
+
padding=get_padding(kernel_size, d),
|
| 115 |
+
)
|
| 116 |
+
)
|
| 117 |
+
for d in dilations
|
| 118 |
+
]
|
| 119 |
+
)
|
| 120 |
+
self.convs2 = nn.ModuleList(
|
| 121 |
+
[
|
| 122 |
+
weight_norm(
|
| 123 |
+
nn.Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1))
|
| 124 |
+
)
|
| 125 |
+
for _ in dilations
|
| 126 |
+
]
|
| 127 |
+
)
|
| 128 |
+
self.acts1 = nn.ModuleList([make_activation(channels, activation) for _ in dilations])
|
| 129 |
+
self.acts2 = nn.ModuleList([make_activation(channels, activation) for _ in dilations])
|
| 130 |
+
|
| 131 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 132 |
+
for c1, c2, a1, a2 in zip(self.convs1, self.convs2, self.acts1, self.acts2):
|
| 133 |
+
y = a1(x)
|
| 134 |
+
y = c1(y)
|
| 135 |
+
y = a2(y)
|
| 136 |
+
y = c2(y)
|
| 137 |
+
x = x + y
|
| 138 |
+
return x
|
| 139 |
+
|
| 140 |
+
def remove_weight_norm(self) -> None:
|
| 141 |
+
for layer in list(self.convs1) + list(self.convs2):
|
| 142 |
+
remove_weight_norm(layer)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class ResBlock2(nn.Module):
|
| 146 |
+
def __init__(self, channels: int, kernel_size: int, dilations: tuple[int, ...], activation: str = "lrelu"):
|
| 147 |
+
super().__init__()
|
| 148 |
+
self.convs = nn.ModuleList(
|
| 149 |
+
[
|
| 150 |
+
weight_norm(
|
| 151 |
+
nn.Conv1d(
|
| 152 |
+
channels,
|
| 153 |
+
channels,
|
| 154 |
+
kernel_size,
|
| 155 |
+
1,
|
| 156 |
+
dilation=d,
|
| 157 |
+
padding=get_padding(kernel_size, d),
|
| 158 |
+
)
|
| 159 |
+
)
|
| 160 |
+
for d in dilations
|
| 161 |
+
]
|
| 162 |
+
)
|
| 163 |
+
self.acts = nn.ModuleList([make_activation(channels, activation) for _ in dilations])
|
| 164 |
+
|
| 165 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 166 |
+
for conv, act in zip(self.convs, self.acts):
|
| 167 |
+
y = act(x)
|
| 168 |
+
y = conv(y)
|
| 169 |
+
x = x + y
|
| 170 |
+
return x
|
| 171 |
+
|
| 172 |
+
def remove_weight_norm(self) -> None:
|
| 173 |
+
for layer in self.convs:
|
| 174 |
+
remove_weight_norm(layer)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class HifiGanGenerator(nn.Module):
|
| 178 |
+
def __init__(self, cfg: HifiGanConfig):
|
| 179 |
+
super().__init__()
|
| 180 |
+
self.cfg = cfg
|
| 181 |
+
self.num_kernels = len(cfg.resblock_kernel_sizes)
|
| 182 |
+
self.num_upsamples = len(cfg.upsample_rates)
|
| 183 |
+
self.conv_pre = weight_norm(
|
| 184 |
+
nn.Conv1d(cfg.num_mels + cfg.conditioning_channels, cfg.upsample_initial_channel, 7, 1, padding=3)
|
| 185 |
+
)
|
| 186 |
+
self.ups = nn.ModuleList()
|
| 187 |
+
self.up_acts = nn.ModuleList()
|
| 188 |
+
self.resblocks = nn.ModuleList()
|
| 189 |
+
resblock_cls = ResBlock1 if cfg.resblock == "1" else ResBlock2
|
| 190 |
+
for i, (rate, kernel) in enumerate(zip(cfg.upsample_rates, cfg.upsample_kernel_sizes)):
|
| 191 |
+
in_ch = cfg.upsample_initial_channel // (2**i)
|
| 192 |
+
out_ch = cfg.upsample_initial_channel // (2 ** (i + 1))
|
| 193 |
+
self.up_acts.append(make_activation(in_ch, cfg.activation))
|
| 194 |
+
self.ups.append(
|
| 195 |
+
weight_norm(
|
| 196 |
+
nn.ConvTranspose1d(
|
| 197 |
+
in_ch,
|
| 198 |
+
out_ch,
|
| 199 |
+
kernel,
|
| 200 |
+
rate,
|
| 201 |
+
padding=(kernel - rate) // 2,
|
| 202 |
+
)
|
| 203 |
+
)
|
| 204 |
+
)
|
| 205 |
+
for k, d in zip(cfg.resblock_kernel_sizes, cfg.resblock_dilation_sizes):
|
| 206 |
+
self.resblocks.append(resblock_cls(out_ch, k, d, cfg.activation))
|
| 207 |
+
final_ch = cfg.upsample_initial_channel // (2 ** len(cfg.upsample_rates))
|
| 208 |
+
self.post_act = make_activation(final_ch, cfg.activation)
|
| 209 |
+
self.conv_post = weight_norm(nn.Conv1d(final_ch, 1, 7, 1, padding=3))
|
| 210 |
+
|
| 211 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 212 |
+
x = self.conv_pre(x)
|
| 213 |
+
for i, up in enumerate(self.ups):
|
| 214 |
+
x = self.up_acts[i](x)
|
| 215 |
+
x = up(x)
|
| 216 |
+
xs = 0.0
|
| 217 |
+
for j in range(self.num_kernels):
|
| 218 |
+
xs = xs + self.resblocks[i * self.num_kernels + j](x)
|
| 219 |
+
x = xs / self.num_kernels
|
| 220 |
+
x = self.post_act(x)
|
| 221 |
+
x = self.conv_post(x)
|
| 222 |
+
return torch.tanh(x)
|
| 223 |
+
|
| 224 |
+
def remove_weight_norm(self) -> None:
|
| 225 |
+
remove_weight_norm(self.conv_pre)
|
| 226 |
+
for up in self.ups:
|
| 227 |
+
remove_weight_norm(up)
|
| 228 |
+
for block in self.resblocks:
|
| 229 |
+
block.remove_weight_norm()
|
| 230 |
+
remove_weight_norm(self.conv_post)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def extract_source_features(
|
| 234 |
+
wav: torch.Tensor,
|
| 235 |
+
cfg: HifiGanConfig,
|
| 236 |
+
frames: int,
|
| 237 |
+
dropout: float = 0.0,
|
| 238 |
+
noise: float = 0.0,
|
| 239 |
+
) -> torch.Tensor:
|
| 240 |
+
"""Return low-rate F0/voicing features for source-conditioned generators."""
|
| 241 |
+
pitch = torchaudio.functional.detect_pitch_frequency(
|
| 242 |
+
wav.detach().cpu(),
|
| 243 |
+
sample_rate=cfg.sample_rate,
|
| 244 |
+
frame_time=cfg.hop_size / cfg.sample_rate,
|
| 245 |
+
win_length=30,
|
| 246 |
+
).to(wav.device)
|
| 247 |
+
if pitch.ndim == 1:
|
| 248 |
+
pitch = pitch.unsqueeze(0)
|
| 249 |
+
if pitch.shape[-1] < frames:
|
| 250 |
+
pitch = F.pad(pitch, (0, frames - pitch.shape[-1]), value=0.0)
|
| 251 |
+
pitch = pitch[..., :frames]
|
| 252 |
+
voiced = ((pitch >= 55.0) & (pitch <= 420.0)).float()
|
| 253 |
+
pitch = pitch.clamp(55.0, 420.0)
|
| 254 |
+
log_f0 = ((torch.log(pitch) - math.log(140.0)) / 0.45).clamp(-3.0, 3.0) * voiced
|
| 255 |
+
if noise > 0.0:
|
| 256 |
+
log_f0 = (log_f0 + torch.randn_like(log_f0) * noise * voiced).clamp(-3.0, 3.0)
|
| 257 |
+
jump = F.pad((log_f0[..., 1:] - log_f0[..., :-1]).abs(), (1, 0))
|
| 258 |
+
confidence = torch.exp(-1.5 * jump) * voiced
|
| 259 |
+
reconstructed_f0 = torch.exp(log_f0 * 0.45 + math.log(140.0))
|
| 260 |
+
phase = torch.cumsum(2.0 * math.pi * reconstructed_f0 * (cfg.hop_size / cfg.sample_rate), dim=-1)
|
| 261 |
+
source = torch.stack(
|
| 262 |
+
[log_f0, voiced, confidence, torch.sin(phase) * confidence, torch.cos(phase) * confidence],
|
| 263 |
+
dim=1,
|
| 264 |
+
)
|
| 265 |
+
if dropout > 0.0:
|
| 266 |
+
# Drop the complete source sketch for some examples so inference remains
|
| 267 |
+
# stable when predicted F0 confidence is poor.
|
| 268 |
+
keep = (torch.rand(source.shape[0], 1, 1, device=source.device) >= dropout).to(source.dtype)
|
| 269 |
+
source = source * keep
|
| 270 |
+
return source
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class DiscriminatorP(nn.Module):
|
| 274 |
+
def __init__(self, period: int):
|
| 275 |
+
super().__init__()
|
| 276 |
+
self.period = period
|
| 277 |
+
self.convs = nn.ModuleList(
|
| 278 |
+
[
|
| 279 |
+
weight_norm(nn.Conv2d(1, 32, (5, 1), (3, 1), padding=(2, 0))),
|
| 280 |
+
weight_norm(nn.Conv2d(32, 128, (5, 1), (3, 1), padding=(2, 0))),
|
| 281 |
+
weight_norm(nn.Conv2d(128, 512, (5, 1), (3, 1), padding=(2, 0))),
|
| 282 |
+
weight_norm(nn.Conv2d(512, 1024, (5, 1), (3, 1), padding=(2, 0))),
|
| 283 |
+
weight_norm(nn.Conv2d(1024, 1024, (5, 1), 1, padding=(2, 0))),
|
| 284 |
+
]
|
| 285 |
+
)
|
| 286 |
+
self.conv_post = weight_norm(nn.Conv2d(1024, 1, (3, 1), 1, padding=(1, 0)))
|
| 287 |
+
|
| 288 |
+
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, list[torch.Tensor]]:
|
| 289 |
+
fmap = []
|
| 290 |
+
b, c, t = x.shape
|
| 291 |
+
if t % self.period != 0:
|
| 292 |
+
x = F.pad(x, (0, self.period - (t % self.period)), mode="reflect")
|
| 293 |
+
t = x.shape[-1]
|
| 294 |
+
x = x.view(b, c, t // self.period, self.period)
|
| 295 |
+
for conv in self.convs:
|
| 296 |
+
x = F.leaky_relu(conv(x), 0.1)
|
| 297 |
+
fmap.append(x)
|
| 298 |
+
x = self.conv_post(x)
|
| 299 |
+
fmap.append(x)
|
| 300 |
+
return torch.flatten(x, 1, -1), fmap
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
class MultiPeriodDiscriminator(nn.Module):
|
| 304 |
+
def __init__(self):
|
| 305 |
+
super().__init__()
|
| 306 |
+
self.discriminators = nn.ModuleList([DiscriminatorP(p) for p in (2, 3, 5, 7, 11)])
|
| 307 |
+
|
| 308 |
+
def forward(self, y: torch.Tensor, y_hat: torch.Tensor):
|
| 309 |
+
y_d_rs, y_d_gs, fmap_rs, fmap_gs = [], [], [], []
|
| 310 |
+
for d in self.discriminators:
|
| 311 |
+
y_d_r, fmap_r = d(y)
|
| 312 |
+
y_d_g, fmap_g = d(y_hat)
|
| 313 |
+
y_d_rs.append(y_d_r)
|
| 314 |
+
y_d_gs.append(y_d_g)
|
| 315 |
+
fmap_rs.append(fmap_r)
|
| 316 |
+
fmap_gs.append(fmap_g)
|
| 317 |
+
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class DiscriminatorS(nn.Module):
|
| 321 |
+
def __init__(self, use_spectral_norm: bool = False):
|
| 322 |
+
super().__init__()
|
| 323 |
+
norm = spectral_norm if use_spectral_norm else weight_norm
|
| 324 |
+
self.convs = nn.ModuleList(
|
| 325 |
+
[
|
| 326 |
+
norm(nn.Conv1d(1, 128, 15, 1, padding=7)),
|
| 327 |
+
norm(nn.Conv1d(128, 128, 41, 2, groups=4, padding=20)),
|
| 328 |
+
norm(nn.Conv1d(128, 256, 41, 2, groups=16, padding=20)),
|
| 329 |
+
norm(nn.Conv1d(256, 512, 41, 4, groups=16, padding=20)),
|
| 330 |
+
norm(nn.Conv1d(512, 1024, 41, 4, groups=16, padding=20)),
|
| 331 |
+
norm(nn.Conv1d(1024, 1024, 41, 1, groups=16, padding=20)),
|
| 332 |
+
norm(nn.Conv1d(1024, 1024, 5, 1, padding=2)),
|
| 333 |
+
]
|
| 334 |
+
)
|
| 335 |
+
self.conv_post = norm(nn.Conv1d(1024, 1, 3, 1, padding=1))
|
| 336 |
+
|
| 337 |
+
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, list[torch.Tensor]]:
|
| 338 |
+
fmap = []
|
| 339 |
+
for conv in self.convs:
|
| 340 |
+
x = F.leaky_relu(conv(x), 0.1)
|
| 341 |
+
fmap.append(x)
|
| 342 |
+
x = self.conv_post(x)
|
| 343 |
+
fmap.append(x)
|
| 344 |
+
return torch.flatten(x, 1, -1), fmap
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
class MultiScaleDiscriminator(nn.Module):
|
| 348 |
+
def __init__(self):
|
| 349 |
+
super().__init__()
|
| 350 |
+
self.discriminators = nn.ModuleList([DiscriminatorS(True), DiscriminatorS(), DiscriminatorS()])
|
| 351 |
+
self.meanpools = nn.ModuleList([nn.AvgPool1d(4, 2, padding=2), nn.AvgPool1d(4, 2, padding=2)])
|
| 352 |
+
|
| 353 |
+
def forward(self, y: torch.Tensor, y_hat: torch.Tensor):
|
| 354 |
+
y_d_rs, y_d_gs, fmap_rs, fmap_gs = [], [], [], []
|
| 355 |
+
for i, d in enumerate(self.discriminators):
|
| 356 |
+
if i:
|
| 357 |
+
y = self.meanpools[i - 1](y)
|
| 358 |
+
y_hat = self.meanpools[i - 1](y_hat)
|
| 359 |
+
y_d_r, fmap_r = d(y)
|
| 360 |
+
y_d_g, fmap_g = d(y_hat)
|
| 361 |
+
y_d_rs.append(y_d_r)
|
| 362 |
+
y_d_gs.append(y_d_g)
|
| 363 |
+
fmap_rs.append(fmap_r)
|
| 364 |
+
fmap_gs.append(fmap_g)
|
| 365 |
+
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class SpectrogramDiscriminator(nn.Module):
|
| 369 |
+
def __init__(self):
|
| 370 |
+
super().__init__()
|
| 371 |
+
channels = (32, 64, 128, 128)
|
| 372 |
+
layers: list[nn.Module] = []
|
| 373 |
+
in_ch = 1
|
| 374 |
+
for out_ch, stride in zip(channels, ((1, 2), (2, 2), (2, 2), (2, 1))):
|
| 375 |
+
layers.append(weight_norm(nn.Conv2d(in_ch, out_ch, (5, 5), stride=stride, padding=(2, 2))))
|
| 376 |
+
in_ch = out_ch
|
| 377 |
+
self.convs = nn.ModuleList(layers)
|
| 378 |
+
self.conv_post = weight_norm(nn.Conv2d(in_ch, 1, (3, 3), padding=(1, 1)))
|
| 379 |
+
|
| 380 |
+
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, list[torch.Tensor]]:
|
| 381 |
+
fmap = []
|
| 382 |
+
for conv in self.convs:
|
| 383 |
+
x = F.leaky_relu(conv(x), 0.1)
|
| 384 |
+
fmap.append(x)
|
| 385 |
+
x = self.conv_post(x)
|
| 386 |
+
fmap.append(x)
|
| 387 |
+
return torch.flatten(x, 1, -1), fmap
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
class MultiResolutionSpectrogramDiscriminator(nn.Module):
|
| 391 |
+
def __init__(self, fft_sizes: tuple[int, ...] = (256, 512, 1024), hop_sizes: tuple[int, ...] = (64, 128, 256), win_lengths: tuple[int, ...] = (256, 512, 1024)):
|
| 392 |
+
super().__init__()
|
| 393 |
+
self.fft_sizes = fft_sizes
|
| 394 |
+
self.hop_sizes = hop_sizes
|
| 395 |
+
self.win_lengths = win_lengths
|
| 396 |
+
self.discriminators = nn.ModuleList([SpectrogramDiscriminator() for _ in fft_sizes])
|
| 397 |
+
|
| 398 |
+
def _features(self, wav: torch.Tensor, fft: int, hop: int, win_len: int) -> torch.Tensor:
|
| 399 |
+
wav = wav.squeeze(1)
|
| 400 |
+
window = torch.hann_window(win_len, device=wav.device)
|
| 401 |
+
spec = torch.stft(wav, n_fft=fft, hop_length=hop, win_length=win_len, window=window, return_complex=True)
|
| 402 |
+
mag = torch.log(spec.abs().clamp_min(1e-5))
|
| 403 |
+
mean = mag.mean(dim=(1, 2), keepdim=True)
|
| 404 |
+
std = mag.std(dim=(1, 2), keepdim=True).clamp_min(1e-4)
|
| 405 |
+
return ((mag - mean) / std).unsqueeze(1)
|
| 406 |
+
|
| 407 |
+
def forward(self, y: torch.Tensor, y_hat: torch.Tensor):
|
| 408 |
+
y_d_rs, y_d_gs, fmap_rs, fmap_gs = [], [], [], []
|
| 409 |
+
for disc, fft, hop, win_len in zip(self.discriminators, self.fft_sizes, self.hop_sizes, self.win_lengths):
|
| 410 |
+
y_feat = self._features(y, fft, hop, win_len)
|
| 411 |
+
y_hat_feat = self._features(y_hat, fft, hop, win_len)
|
| 412 |
+
y_d_r, fmap_r = disc(y_feat)
|
| 413 |
+
y_d_g, fmap_g = disc(y_hat_feat)
|
| 414 |
+
y_d_rs.append(y_d_r)
|
| 415 |
+
y_d_gs.append(y_d_g)
|
| 416 |
+
fmap_rs.append(fmap_r)
|
| 417 |
+
fmap_gs.append(fmap_g)
|
| 418 |
+
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
class MelFrontend(nn.Module):
|
| 422 |
+
def __init__(self, cfg: HifiGanConfig):
|
| 423 |
+
super().__init__()
|
| 424 |
+
self.mel = torchaudio.transforms.MelSpectrogram(
|
| 425 |
+
sample_rate=cfg.sample_rate,
|
| 426 |
+
n_fft=cfg.n_fft,
|
| 427 |
+
win_length=cfg.win_size,
|
| 428 |
+
hop_length=cfg.hop_size,
|
| 429 |
+
f_min=cfg.fmin,
|
| 430 |
+
f_max=cfg.fmax,
|
| 431 |
+
n_mels=cfg.num_mels,
|
| 432 |
+
power=1.0,
|
| 433 |
+
center=True,
|
| 434 |
+
norm="slaney",
|
| 435 |
+
mel_scale="slaney",
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
def forward(self, wav: torch.Tensor) -> torch.Tensor:
|
| 439 |
+
return torch.log(torch.clamp(self.mel(wav), min=1e-5))
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def load_rows(path: Path, max_rows: int, min_seconds: float, max_seconds: float) -> list[dict]:
|
| 443 |
+
rows = []
|
| 444 |
+
with path.open("r", encoding="utf-8-sig") as f:
|
| 445 |
+
for line in f:
|
| 446 |
+
if not line.strip():
|
| 447 |
+
continue
|
| 448 |
+
row = json.loads(line)
|
| 449 |
+
audio = Path(str(row.get("target_audio") or ""))
|
| 450 |
+
text = str(row.get("target_text") or "").strip()
|
| 451 |
+
dur = float(row.get("target_duration_s") or 0.0)
|
| 452 |
+
if audio.is_file() and text and min_seconds <= (dur or 4.0) <= max_seconds:
|
| 453 |
+
rows.append({"audio": str(audio), "text": text, "duration": dur})
|
| 454 |
+
if max_rows > 0 and len(rows) >= max_rows:
|
| 455 |
+
break
|
| 456 |
+
if not rows:
|
| 457 |
+
raise RuntimeError(f"No rows loaded from {path}")
|
| 458 |
+
return rows
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def load_audio(path: str, sample_rate: int) -> torch.Tensor:
|
| 462 |
+
import soundfile as _sf # avoid torchaudio.load (needs torchcodec/ffmpeg on torch>=2.1)
|
| 463 |
+
_a, sr = _sf.read(path, dtype="float32", always_2d=True)
|
| 464 |
+
wav = torch.from_numpy(_a.T) # [ch, T]
|
| 465 |
+
if wav.shape[0] > 1:
|
| 466 |
+
wav = wav.mean(dim=0, keepdim=True)
|
| 467 |
+
if sr != sample_rate:
|
| 468 |
+
wav = torchaudio.functional.resample(wav, sr, sample_rate)
|
| 469 |
+
wav = wav.squeeze(0)
|
| 470 |
+
return wav.clamp(-1, 1)
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
class AudioDataset(Dataset):
|
| 474 |
+
def __init__(self, rows: list[dict], cfg: HifiGanConfig, segment_size: int, seed: int):
|
| 475 |
+
self.rows = rows
|
| 476 |
+
self.cfg = cfg
|
| 477 |
+
self.segment_size = segment_size
|
| 478 |
+
self.rng = random.Random(seed)
|
| 479 |
+
|
| 480 |
+
def __len__(self) -> int:
|
| 481 |
+
return len(self.rows)
|
| 482 |
+
|
| 483 |
+
def __getitem__(self, idx: int) -> torch.Tensor:
|
| 484 |
+
wav = load_audio(self.rows[idx]["audio"], self.cfg.sample_rate)
|
| 485 |
+
if wav.numel() >= self.segment_size:
|
| 486 |
+
start = self.rng.randint(0, wav.numel() - self.segment_size)
|
| 487 |
+
return wav[start : start + self.segment_size]
|
| 488 |
+
return F.pad(wav, (0, self.segment_size - wav.numel()))
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def feature_loss(fmap_r, fmap_g) -> torch.Tensor:
|
| 492 |
+
loss = 0.0
|
| 493 |
+
for dr, dg in zip(fmap_r, fmap_g):
|
| 494 |
+
for rl, gl in zip(dr, dg):
|
| 495 |
+
loss = loss + F.l1_loss(rl.detach(), gl)
|
| 496 |
+
return loss * 2
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def discriminator_loss(disc_real_outputs, disc_generated_outputs) -> torch.Tensor:
|
| 500 |
+
loss = 0.0
|
| 501 |
+
for dr, dg in zip(disc_real_outputs, disc_generated_outputs):
|
| 502 |
+
loss = loss + torch.mean((1 - dr) ** 2) + torch.mean(dg**2)
|
| 503 |
+
return loss
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def generator_loss(disc_outputs) -> torch.Tensor:
|
| 507 |
+
loss = 0.0
|
| 508 |
+
for dg in disc_outputs:
|
| 509 |
+
loss = loss + torch.mean((1 - dg) ** 2)
|
| 510 |
+
return loss
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def stft_mag_loss(y_hat: torch.Tensor, y: torch.Tensor, fft_sizes: tuple[int, ...], hop_sizes: tuple[int, ...], win_lengths: tuple[int, ...]) -> torch.Tensor:
|
| 514 |
+
# Multi-resolution spectral loss catches buzz/shimmer that can hide behind
|
| 515 |
+
# mel loss, especially for a small generator near convergence.
|
| 516 |
+
y_hat = y_hat.squeeze(1)
|
| 517 |
+
y = y.squeeze(1)
|
| 518 |
+
total = torch.zeros((), device=y.device)
|
| 519 |
+
for fft, hop, win_len in zip(fft_sizes, hop_sizes, win_lengths):
|
| 520 |
+
window = torch.hann_window(win_len, device=y.device)
|
| 521 |
+
pred = torch.stft(y_hat, n_fft=fft, hop_length=hop, win_length=win_len, window=window, return_complex=True)
|
| 522 |
+
target = torch.stft(y, n_fft=fft, hop_length=hop, win_length=win_len, window=window, return_complex=True)
|
| 523 |
+
pred_mag = pred.abs().clamp_min(1e-7)
|
| 524 |
+
target_mag = target.abs().clamp_min(1e-7)
|
| 525 |
+
sc = torch.linalg.vector_norm(target_mag - pred_mag) / torch.linalg.vector_norm(target_mag).clamp_min(1e-7)
|
| 526 |
+
log_mag = F.l1_loss(torch.log(pred_mag), torch.log(target_mag))
|
| 527 |
+
total = total + sc + log_mag
|
| 528 |
+
return total / max(1, len(fft_sizes))
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
def count_parameters(module: nn.Module) -> int:
|
| 532 |
+
return sum(p.numel() for p in module.parameters())
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def jsonable_args(args: argparse.Namespace) -> dict:
|
| 536 |
+
return {k: str(v) if isinstance(v, Path) else v for k, v in vars(args).items()}
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def save_checkpoint(
|
| 540 |
+
path: Path,
|
| 541 |
+
generator: nn.Module,
|
| 542 |
+
mpd: nn.Module,
|
| 543 |
+
msd: nn.Module,
|
| 544 |
+
optim_g,
|
| 545 |
+
optim_d,
|
| 546 |
+
cfg: HifiGanConfig,
|
| 547 |
+
step: int,
|
| 548 |
+
args,
|
| 549 |
+
mrsd: nn.Module | None = None,
|
| 550 |
+
) -> None:
|
| 551 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 552 |
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
| 553 |
+
payload = {
|
| 554 |
+
"generator": generator.state_dict(),
|
| 555 |
+
"mpd": mpd.state_dict(),
|
| 556 |
+
"msd": msd.state_dict(),
|
| 557 |
+
"optim_g": optim_g.state_dict(),
|
| 558 |
+
"optim_d": optim_d.state_dict(),
|
| 559 |
+
"config": asdict(cfg),
|
| 560 |
+
"step": step,
|
| 561 |
+
"args": jsonable_args(args),
|
| 562 |
+
"generator_params": count_parameters(generator),
|
| 563 |
+
}
|
| 564 |
+
if mrsd is not None:
|
| 565 |
+
payload["mrsd"] = mrsd.state_dict()
|
| 566 |
+
torch.save(payload, tmp)
|
| 567 |
+
tmp.replace(path)
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def checkpoint_step(path: Path) -> int:
|
| 571 |
+
stem = path.stem
|
| 572 |
+
tail = stem.rsplit("-", 1)[-1]
|
| 573 |
+
return int(tail) if tail.isdigit() else -1
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def prune_checkpoints(out_dir: Path, variant: str, keep: int) -> None:
|
| 577 |
+
if keep <= 0:
|
| 578 |
+
return
|
| 579 |
+
numbered = [p for p in out_dir.glob(f"hifigan-{variant}-*.pt") if checkpoint_step(p) >= 0]
|
| 580 |
+
numbered.sort(key=checkpoint_step, reverse=True)
|
| 581 |
+
for old in numbered[keep:]:
|
| 582 |
+
old.unlink(missing_ok=True)
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def latest_checkpoint(out_dir: Path) -> Path | None:
|
| 586 |
+
numbered = [p for p in out_dir.glob("hifigan-*-*.pt") if checkpoint_step(p) >= 0]
|
| 587 |
+
if numbered:
|
| 588 |
+
return max(numbered, key=checkpoint_step)
|
| 589 |
+
ckpts = sorted(out_dir.glob("hifigan-*-latest.pt"), key=lambda p: p.stat().st_mtime, reverse=True)
|
| 590 |
+
return ckpts[0] if ckpts else None
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def partial_load_state(module: nn.Module, state: dict[str, torch.Tensor]) -> tuple[int, int]:
|
| 594 |
+
current = module.state_dict()
|
| 595 |
+
patched: dict[str, torch.Tensor] = {}
|
| 596 |
+
copied = 0
|
| 597 |
+
skipped = 0
|
| 598 |
+
for name, target in current.items():
|
| 599 |
+
source = state.get(name)
|
| 600 |
+
if source is None:
|
| 601 |
+
skipped += 1
|
| 602 |
+
continue
|
| 603 |
+
if source.shape == target.shape:
|
| 604 |
+
patched[name] = source
|
| 605 |
+
copied += 1
|
| 606 |
+
continue
|
| 607 |
+
if source.ndim != target.ndim:
|
| 608 |
+
skipped += 1
|
| 609 |
+
continue
|
| 610 |
+
value = target.clone()
|
| 611 |
+
slices = tuple(slice(0, min(a, b)) for a, b in zip(target.shape, source.shape))
|
| 612 |
+
value[slices] = source[slices].to(value.device, value.dtype)
|
| 613 |
+
patched[name] = value
|
| 614 |
+
copied += 1
|
| 615 |
+
module.load_state_dict(patched, strict=False)
|
| 616 |
+
return copied, skipped
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def train(args: argparse.Namespace) -> None:
|
| 620 |
+
torch.backends.cudnn.benchmark = True
|
| 621 |
+
cfg = make_config(args.variant)
|
| 622 |
+
device = torch.device(args.device)
|
| 623 |
+
rows = load_rows(args.train_jsonl, args.max_rows, args.min_seconds, args.max_seconds)
|
| 624 |
+
rng = random.Random(args.seed)
|
| 625 |
+
rng.shuffle(rows)
|
| 626 |
+
dataset = AudioDataset(rows, cfg, args.segment_size, args.seed)
|
| 627 |
+
loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=True, drop_last=True, num_workers=args.num_workers)
|
| 628 |
+
mel_frontend = MelFrontend(cfg).to(device)
|
| 629 |
+
generator = HifiGanGenerator(cfg).to(device)
|
| 630 |
+
mpd = MultiPeriodDiscriminator().to(device)
|
| 631 |
+
msd = MultiScaleDiscriminator().to(device)
|
| 632 |
+
mrsd = MultiResolutionSpectrogramDiscriminator().to(device) if args.spec_disc_weight > 0.0 else None
|
| 633 |
+
optim_g = torch.optim.AdamW(generator.parameters(), lr=args.lr, betas=(0.8, 0.99))
|
| 634 |
+
disc_params = list(mpd.parameters()) + list(msd.parameters())
|
| 635 |
+
if mrsd is not None:
|
| 636 |
+
disc_params += list(mrsd.parameters())
|
| 637 |
+
optim_d = torch.optim.AdamW(disc_params, lr=args.lr, betas=(0.8, 0.99))
|
| 638 |
+
start_step = 0
|
| 639 |
+
if args.init_checkpoint and not args.resume:
|
| 640 |
+
ckpt = torch.load(args.init_checkpoint, map_location=device, weights_only=False)
|
| 641 |
+
if args.partial_init:
|
| 642 |
+
copied, skipped = partial_load_state(generator, ckpt["generator"])
|
| 643 |
+
print(f"Partially initialized generator from {args.init_checkpoint}: copied={copied} skipped={skipped}")
|
| 644 |
+
else:
|
| 645 |
+
generator.load_state_dict(ckpt["generator"])
|
| 646 |
+
if "mpd" in ckpt and "msd" in ckpt:
|
| 647 |
+
mpd.load_state_dict(ckpt["mpd"])
|
| 648 |
+
msd.load_state_dict(ckpt["msd"])
|
| 649 |
+
if mrsd is not None and "mrsd" in ckpt:
|
| 650 |
+
mrsd.load_state_dict(ckpt["mrsd"])
|
| 651 |
+
can_load_disc_optim = mrsd is None or "mrsd" in ckpt
|
| 652 |
+
if not args.partial_init and not args.reset_optim and "optim_g" in ckpt:
|
| 653 |
+
optim_g.load_state_dict(ckpt["optim_g"])
|
| 654 |
+
if not args.partial_init and not args.reset_optim and can_load_disc_optim and "optim_d" in ckpt:
|
| 655 |
+
optim_d.load_state_dict(ckpt["optim_d"])
|
| 656 |
+
for group in optim_g.param_groups:
|
| 657 |
+
group["lr"] = args.lr
|
| 658 |
+
for group in optim_d.param_groups:
|
| 659 |
+
group["lr"] = args.lr
|
| 660 |
+
start_step = int(ckpt.get("step") or 0)
|
| 661 |
+
print(f"Initialized {args.init_checkpoint} at step {start_step}; lr={args.lr:g}")
|
| 662 |
+
if args.resume:
|
| 663 |
+
ckpt_path = latest_checkpoint(args.out_dir)
|
| 664 |
+
if ckpt_path:
|
| 665 |
+
ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
|
| 666 |
+
generator.load_state_dict(ckpt["generator"])
|
| 667 |
+
mpd.load_state_dict(ckpt["mpd"])
|
| 668 |
+
msd.load_state_dict(ckpt["msd"])
|
| 669 |
+
if mrsd is not None and "mrsd" in ckpt:
|
| 670 |
+
mrsd.load_state_dict(ckpt["mrsd"])
|
| 671 |
+
optim_g.load_state_dict(ckpt["optim_g"])
|
| 672 |
+
optim_d.load_state_dict(ckpt["optim_d"])
|
| 673 |
+
for group in optim_g.param_groups:
|
| 674 |
+
group["lr"] = args.lr
|
| 675 |
+
for group in optim_d.param_groups:
|
| 676 |
+
group["lr"] = args.lr
|
| 677 |
+
start_step = int(ckpt.get("step") or 0)
|
| 678 |
+
print(f"Resumed {ckpt_path} at step {start_step}; lr={args.lr:g}")
|
| 679 |
+
|
| 680 |
+
args.out_dir.mkdir(parents=True, exist_ok=True)
|
| 681 |
+
prune_checkpoints(args.out_dir, args.variant, args.keep_checkpoints)
|
| 682 |
+
(args.out_dir / "config.json").write_text(
|
| 683 |
+
json.dumps(
|
| 684 |
+
{
|
| 685 |
+
"config": asdict(cfg),
|
| 686 |
+
"args": jsonable_args(args),
|
| 687 |
+
"rows": len(rows),
|
| 688 |
+
"generator_params": count_parameters(generator),
|
| 689 |
+
"mpd_params": count_parameters(mpd),
|
| 690 |
+
"msd_params": count_parameters(msd),
|
| 691 |
+
"mrsd_params": count_parameters(mrsd) if mrsd is not None else 0,
|
| 692 |
+
},
|
| 693 |
+
indent=2,
|
| 694 |
+
),
|
| 695 |
+
encoding="utf-8",
|
| 696 |
+
)
|
| 697 |
+
print(f"Variant: {args.variant}")
|
| 698 |
+
print(f"Rows: {len(rows)}")
|
| 699 |
+
print(f"Generator params: {count_parameters(generator):,} ({count_parameters(generator)/1_000_000:.3f}M)")
|
| 700 |
+
print(f"MPD params: {count_parameters(mpd):,} MSD params: {count_parameters(msd):,} (training only)")
|
| 701 |
+
if mrsd is not None:
|
| 702 |
+
print(f"MRSD params: {count_parameters(mrsd):,} (training only)")
|
| 703 |
+
if args.steps == 0:
|
| 704 |
+
return
|
| 705 |
+
|
| 706 |
+
step = start_step
|
| 707 |
+
started = time.time()
|
| 708 |
+
try:
|
| 709 |
+
while step < args.steps:
|
| 710 |
+
for wav in loader:
|
| 711 |
+
step += 1
|
| 712 |
+
y = wav.unsqueeze(1).to(device)
|
| 713 |
+
with torch.no_grad():
|
| 714 |
+
mel = mel_frontend(wav.to(device))
|
| 715 |
+
if cfg.conditioning_channels:
|
| 716 |
+
source = extract_source_features(
|
| 717 |
+
wav.to(device),
|
| 718 |
+
cfg,
|
| 719 |
+
mel.shape[-1],
|
| 720 |
+
dropout=args.source_dropout,
|
| 721 |
+
noise=args.source_noise,
|
| 722 |
+
)
|
| 723 |
+
generator_input = torch.cat([mel, source], dim=1)
|
| 724 |
+
else:
|
| 725 |
+
generator_input = mel
|
| 726 |
+
y_hat = generator(generator_input)
|
| 727 |
+
common = min(y.shape[-1], y_hat.shape[-1])
|
| 728 |
+
y = y[..., :common]
|
| 729 |
+
y_hat = y_hat[..., :common]
|
| 730 |
+
y_mel = mel_frontend(y.squeeze(1))
|
| 731 |
+
y_hat_mel = mel_frontend(y_hat.squeeze(1))
|
| 732 |
+
|
| 733 |
+
optim_d.zero_grad(set_to_none=True)
|
| 734 |
+
y_df_hat_r, y_df_hat_g, _, _ = mpd(y, y_hat.detach())
|
| 735 |
+
y_ds_hat_r, y_ds_hat_g, _, _ = msd(y, y_hat.detach())
|
| 736 |
+
loss_disc = discriminator_loss(y_df_hat_r, y_df_hat_g) + discriminator_loss(y_ds_hat_r, y_ds_hat_g)
|
| 737 |
+
loss_spec_disc = torch.zeros((), device=device)
|
| 738 |
+
if mrsd is not None:
|
| 739 |
+
y_dm_hat_r, y_dm_hat_g, _, _ = mrsd(y, y_hat.detach())
|
| 740 |
+
loss_spec_disc = discriminator_loss(y_dm_hat_r, y_dm_hat_g)
|
| 741 |
+
loss_disc = loss_disc + args.spec_disc_weight * loss_spec_disc
|
| 742 |
+
loss_disc.backward()
|
| 743 |
+
torch.nn.utils.clip_grad_norm_(disc_params, args.grad_clip)
|
| 744 |
+
optim_d.step()
|
| 745 |
+
|
| 746 |
+
optim_g.zero_grad(set_to_none=True)
|
| 747 |
+
mel_loss = F.l1_loss(y_mel, y_hat_mel) * args.mel_weight
|
| 748 |
+
y_df_hat_r, y_df_hat_g, fmap_f_r, fmap_f_g = mpd(y, y_hat)
|
| 749 |
+
y_ds_hat_r, y_ds_hat_g, fmap_s_r, fmap_s_g = msd(y, y_hat)
|
| 750 |
+
loss_fm = feature_loss(fmap_f_r, fmap_f_g) + feature_loss(fmap_s_r, fmap_s_g)
|
| 751 |
+
loss_gen = generator_loss(y_df_hat_g) + generator_loss(y_ds_hat_g)
|
| 752 |
+
loss_spec_gen = torch.zeros((), device=device)
|
| 753 |
+
loss_spec_fm = torch.zeros((), device=device)
|
| 754 |
+
if mrsd is not None:
|
| 755 |
+
y_dm_hat_r, y_dm_hat_g, fmap_m_r, fmap_m_g = mrsd(y, y_hat)
|
| 756 |
+
loss_spec_gen = generator_loss(y_dm_hat_g)
|
| 757 |
+
loss_spec_fm = feature_loss(fmap_m_r, fmap_m_g)
|
| 758 |
+
wav_l1 = F.l1_loss(y_hat, y) * args.wav_weight
|
| 759 |
+
stft_loss = torch.zeros((), device=device)
|
| 760 |
+
if args.stft_weight > 0.0:
|
| 761 |
+
stft_loss = stft_mag_loss(y_hat, y, (512, 1024, 2048), (128, 256, 512), (512, 1024, 2048)) * args.stft_weight
|
| 762 |
+
loss_g = (
|
| 763 |
+
mel_loss
|
| 764 |
+
+ args.fm_weight * loss_fm
|
| 765 |
+
+ args.adv_weight * loss_gen
|
| 766 |
+
+ wav_l1
|
| 767 |
+
+ stft_loss
|
| 768 |
+
+ args.spec_disc_weight * loss_spec_gen
|
| 769 |
+
+ args.spec_fm_weight * loss_spec_fm
|
| 770 |
+
)
|
| 771 |
+
loss_g.backward()
|
| 772 |
+
grad_g = torch.nn.utils.clip_grad_norm_(generator.parameters(), args.grad_clip)
|
| 773 |
+
optim_g.step()
|
| 774 |
+
|
| 775 |
+
if step == 1 or step % args.log_interval == 0:
|
| 776 |
+
elapsed = max(time.time() - started, 1e-6)
|
| 777 |
+
speed = (step - start_step) / elapsed
|
| 778 |
+
eta = (args.steps - step) / max(speed, 1e-6)
|
| 779 |
+
print(
|
| 780 |
+
f"step={step}/{args.steps} g={loss_g.item():.4f} d={loss_disc.item():.4f} "
|
| 781 |
+
f"mel={mel_loss.item():.4f} fm={loss_fm.item():.4f} adv={loss_gen.item():.4f} "
|
| 782 |
+
f"wav={wav_l1.item():.4f} stft={stft_loss.item():.4f} "
|
| 783 |
+
f"sd={loss_spec_disc.item():.4f} sfm={loss_spec_fm.item():.4f} sadv={loss_spec_gen.item():.4f} "
|
| 784 |
+
f"grad={float(grad_g):.3f} speed={speed:.3f} step/s eta={eta/60:.1f}m",
|
| 785 |
+
flush=True,
|
| 786 |
+
)
|
| 787 |
+
if step % args.save_interval == 0 or step >= args.steps:
|
| 788 |
+
prune_checkpoints(args.out_dir, args.variant, max(args.keep_checkpoints - 1, 0))
|
| 789 |
+
save_checkpoint(args.out_dir / f"hifigan-{args.variant}-{step}.pt", generator, mpd, msd, optim_g, optim_d, cfg, step, args, mrsd)
|
| 790 |
+
save_checkpoint(args.out_dir / f"hifigan-{args.variant}-latest.pt", generator, mpd, msd, optim_g, optim_d, cfg, step, args, mrsd)
|
| 791 |
+
if step >= args.steps:
|
| 792 |
+
break
|
| 793 |
+
except KeyboardInterrupt:
|
| 794 |
+
if step > start_step:
|
| 795 |
+
save_checkpoint(args.out_dir / f"hifigan-{args.variant}-interrupt-{step}.pt", generator, mpd, msd, optim_g, optim_d, cfg, step, args, mrsd)
|
| 796 |
+
save_checkpoint(args.out_dir / f"hifigan-{args.variant}-latest.pt", generator, mpd, msd, optim_g, optim_d, cfg, step, args, mrsd)
|
| 797 |
+
print(f"Interrupted. Saved checkpoint at step {step}.", flush=True)
|
| 798 |
+
raise
|
| 799 |
+
save_checkpoint(args.out_dir / f"hifigan-{args.variant}-final.pt", generator, mpd, msd, optim_g, optim_d, cfg, step, args, mrsd)
|
| 800 |
+
print(f"Done. {args.out_dir}")
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
def main() -> None:
|
| 804 |
+
ap = argparse.ArgumentParser(description="Train exact-ish HiFi-GAN V2/V3 oracle vocoders on corrected Mark audio.")
|
| 805 |
+
ap.add_argument("--train-jsonl", type=Path, required=True)
|
| 806 |
+
ap.add_argument("--out-dir", type=Path, required=True)
|
| 807 |
+
ap.add_argument(
|
| 808 |
+
"--variant",
|
| 809 |
+
choices=["v2", "v2plus", "v2wide", "snake_v2mid", "snake_8k", "snake_8k40", "snake_v2balanced", "source_snake_v2balanced", "v3"],
|
| 810 |
+
required=True,
|
| 811 |
+
)
|
| 812 |
+
ap.add_argument("--steps", type=int, default=5000)
|
| 813 |
+
ap.add_argument("--max-rows", type=int, default=0)
|
| 814 |
+
ap.add_argument("--min-seconds", type=float, default=1.0)
|
| 815 |
+
ap.add_argument("--max-seconds", type=float, default=12.0)
|
| 816 |
+
ap.add_argument("--segment-size", type=int, default=8192)
|
| 817 |
+
ap.add_argument("--batch-size", type=int, default=8)
|
| 818 |
+
ap.add_argument("--num-workers", type=int, default=0)
|
| 819 |
+
ap.add_argument("--lr", type=float, default=2.0e-4)
|
| 820 |
+
ap.add_argument("--mel-weight", type=float, default=45.0)
|
| 821 |
+
ap.add_argument("--wav-weight", type=float, default=1.0)
|
| 822 |
+
ap.add_argument("--fm-weight", type=float, default=1.0)
|
| 823 |
+
ap.add_argument("--adv-weight", type=float, default=1.0)
|
| 824 |
+
ap.add_argument("--stft-weight", type=float, default=0.0)
|
| 825 |
+
ap.add_argument("--spec-disc-weight", type=float, default=0.0, help="Training-only multi-resolution spectrogram adversarial weight.")
|
| 826 |
+
ap.add_argument("--spec-fm-weight", type=float, default=0.0, help="Training-only spectrogram discriminator feature-matching weight.")
|
| 827 |
+
ap.add_argument("--source-dropout", type=float, default=0.0, help="Probability of dropping source conditioning per training example.")
|
| 828 |
+
ap.add_argument("--source-noise", type=float, default=0.0, help="Stddev of normalized log-F0 corruption for source conditioning.")
|
| 829 |
+
ap.add_argument("--grad-clip", type=float, default=1000.0)
|
| 830 |
+
ap.add_argument("--log-interval", type=int, default=50)
|
| 831 |
+
ap.add_argument("--save-interval", type=int, default=1000)
|
| 832 |
+
ap.add_argument("--keep-checkpoints", type=int, default=12)
|
| 833 |
+
ap.add_argument("--seed", type=int, default=1234)
|
| 834 |
+
ap.add_argument("--device", default="cuda")
|
| 835 |
+
ap.add_argument("--resume", action="store_true")
|
| 836 |
+
ap.add_argument("--init-checkpoint", type=Path)
|
| 837 |
+
ap.add_argument("--partial-init", action="store_true", help="Slice-copy compatible generator weights from init-checkpoint into a resized generator.")
|
| 838 |
+
ap.add_argument("--reset-optim", action="store_true", help="When initializing from a checkpoint, load model/discriminators but start fresh optimizers.")
|
| 839 |
+
args = ap.parse_args()
|
| 840 |
+
train(args)
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
if __name__ == "__main__":
|
| 844 |
+
main()
|
scripts/align_durations_v4.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""v4 aligner: PHONE-LEVEL forced alignment via espeak phoneme-CTC.
|
| 3 |
+
|
| 4 |
+
Root-cause fix. v1/v3 did CHAR-level CTC then heuristically SPLIT each char span
|
| 5 |
+
across its phones -> wrong RELATIVE phone durations -> over-smoothed acoustic.
|
| 6 |
+
v4 aligns OUR frontend's phone sequence DIRECTLY:
|
| 7 |
+
each phone -> one IPA token in facebook/wav2vec2-lv-60-espeak-cv-ft's vocab
|
| 8 |
+
-> torchaudio.forced_align(emissions, our_phone_ipa_targets) -> per-phone frames.
|
| 9 |
+
No splitting. Durations are contiguous (cover all frames); fit_durations() rescales
|
| 10 |
+
to true mel length at train time, so only RELATIVE proportions matter -- and those
|
| 11 |
+
are now correct from the phone recognizer.
|
| 12 |
+
|
| 13 |
+
Non-alignable phones (SP / punctuation / OOV) carry a small relative value.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
import argparse, json, sys
|
| 17 |
+
import numpy as np, torch, soundfile as sf, librosa
|
| 18 |
+
sys.path.insert(0, "/home/luigi/jetson-tts/mossnano/zhtw8k")
|
| 19 |
+
|
| 20 |
+
ESPEAK = "facebook/wav2vec2-lv-60-espeak-cv-ft"
|
| 21 |
+
|
| 22 |
+
# ARPABET (en, stress stripped) -> single IPA token present in the espeak vocab.
|
| 23 |
+
ARPA2IPA = {
|
| 24 |
+
'AA':'Ι','AE':'Γ¦','AH':'Κ','AO':'Ι','AW':'aΚ','AY':'aΙͺ','B':'b','CH':'tΚ',
|
| 25 |
+
'D':'d','DH':'Γ°','EH':'Ι','ER':'Ι','EY':'eΙͺ','F':'f','G':'Ι‘','HH':'h',
|
| 26 |
+
'IH':'Ιͺ','IY':'i','JH':'dΚ','K':'k','L':'l','M':'m','N':'n','NG':'Ε',
|
| 27 |
+
'OW':'oΚ','OY':'ΙΙͺ','P':'p','R':'ΙΉ','S':'s','SH':'Κ','T':'t','TH':'ΞΈ',
|
| 28 |
+
'UH':'Κ','UW':'u','V':'v','W':'w','Y':'j','Z':'z','ZH':'Κ',
|
| 29 |
+
}
|
| 30 |
+
# bopomofo (zh) -> toneless IPA token in the espeak Mandarin inventory (best-effort).
|
| 31 |
+
BOPO2IPA = {
|
| 32 |
+
'γ
':'p','γ':'pΚ°','γ':'m','γ':'f','γ':'t','γ':'tΚ°','γ':'n','γ':'l',
|
| 33 |
+
'γ':'k','γ':'kΚ°','γ':'x','γ':'tΙ','γ':'tΙh','γ':'Ι','γ':'tΚ','γ':'tΚΚ°',
|
| 34 |
+
'γ':'Κ','γ':'Κ','γ':'ts','γ':'tsh','γ':'s',
|
| 35 |
+
'γ§':'i','γ¨':'u','γ©':'y','γ':'a','γ':'o','γ':'Ι€','γ':'e','γ':'ai',
|
| 36 |
+
'γ':'ei','γ ':'au','γ‘':'ou','γ’':'a','γ£':'Ι','γ€':'Ι','γ₯':'Ι','γ¦':'Ι',
|
| 37 |
+
'γ':'Ι¨',
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_frontend_with_charidx():
|
| 42 |
+
import frontend_bopomofo as F
|
| 43 |
+
import re
|
| 44 |
+
F._lazy()
|
| 45 |
+
def run(text):
|
| 46 |
+
text = F._zh_num(text)
|
| 47 |
+
bopo = F._g2pw(text)[0]
|
| 48 |
+
chars = list(text)
|
| 49 |
+
phones, tones, langs = [], [], []
|
| 50 |
+
i = 0
|
| 51 |
+
while i < len(chars):
|
| 52 |
+
b = bopo[i] if i < len(bopo) else None
|
| 53 |
+
ch = chars[i]
|
| 54 |
+
if b is not None:
|
| 55 |
+
units, tone = F._split_syllable(b)
|
| 56 |
+
for u in units:
|
| 57 |
+
phones.append(u); tones.append(min(tone,5)); langs.append(0)
|
| 58 |
+
i += 1
|
| 59 |
+
elif re.match(r'[A-Za-z]', ch):
|
| 60 |
+
j = i
|
| 61 |
+
while j < len(chars) and re.match(r"[A-Za-z']", chars[j]): j += 1
|
| 62 |
+
for p in F._g2pen(''.join(chars[i:j])):
|
| 63 |
+
p = p.strip()
|
| 64 |
+
if not p: continue
|
| 65 |
+
if p[-1].isdigit(): st=int(p[-1]); p=p[:-1]
|
| 66 |
+
else: st=0
|
| 67 |
+
if p in F.SYM2ID:
|
| 68 |
+
phones.append(p); tones.append(st); langs.append(1)
|
| 69 |
+
phones.append('SP'); tones.append(0); langs.append(1)
|
| 70 |
+
i = j
|
| 71 |
+
else:
|
| 72 |
+
if ch in F.PUNCT:
|
| 73 |
+
phones.append(ch); tones.append(0); langs.append(0)
|
| 74 |
+
elif ch.strip()=='' and phones and phones[-1]!='SP':
|
| 75 |
+
phones.append('SP'); tones.append(0); langs.append(0)
|
| 76 |
+
i += 1
|
| 77 |
+
return text, phones, tones, langs
|
| 78 |
+
return run, F
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Aligner:
|
| 82 |
+
def __init__(self, device):
|
| 83 |
+
from transformers import Wav2Vec2ForCTC
|
| 84 |
+
from huggingface_hub import hf_hub_download
|
| 85 |
+
self.vocab = json.load(open(hf_hub_download(ESPEAK, "vocab.json")))
|
| 86 |
+
self.blank = self.vocab["<pad>"]
|
| 87 |
+
self.model = Wav2Vec2ForCTC.from_pretrained(ESPEAK).to(device).eval()
|
| 88 |
+
self.device = device
|
| 89 |
+
|
| 90 |
+
def emissions(self, audio16k):
|
| 91 |
+
iv = torch.from_numpy(audio16k).float().unsqueeze(0).to(self.device)
|
| 92 |
+
with torch.inference_mode():
|
| 93 |
+
logits = self.model(iv).logits[0] # [T,V]
|
| 94 |
+
return torch.log_softmax(logits, dim=-1).cpu()
|
| 95 |
+
|
| 96 |
+
def durations(self, audio16k, phones, langs):
|
| 97 |
+
"""Return integer relative duration per phone (contiguous CTC spans)."""
|
| 98 |
+
from torchaudio.functional import forced_align
|
| 99 |
+
emis = self.emissions(audio16k)
|
| 100 |
+
T = emis.shape[0]
|
| 101 |
+
# target = alignable phones mapped to a vocab token id
|
| 102 |
+
tgt_ids, tgt_pi = [], []
|
| 103 |
+
for pi, (p, lg) in enumerate(zip(phones, langs)):
|
| 104 |
+
tok = ARPA2IPA.get(p) if lg == 1 else BOPO2IPA.get(p)
|
| 105 |
+
if tok is not None and tok in self.vocab:
|
| 106 |
+
tgt_ids.append(self.vocab[tok]); tgt_pi.append(pi)
|
| 107 |
+
n = len(phones); dur = [0.0]*n
|
| 108 |
+
if len(tgt_ids) < 1 or len(tgt_ids) > T:
|
| 109 |
+
# fall back: uniform
|
| 110 |
+
for pi in range(n): dur[pi] = 1.0
|
| 111 |
+
return [max(1,int(round(x))) for x in dur], 0
|
| 112 |
+
tokens = torch.tensor([tgt_ids], dtype=torch.int32)
|
| 113 |
+
try:
|
| 114 |
+
aligned, _ = forced_align(emis.unsqueeze(0), tokens, blank=self.blank)
|
| 115 |
+
except Exception:
|
| 116 |
+
for pi in range(n): dur[pi] = 1.0
|
| 117 |
+
return [max(1,int(round(x))) for x in dur], 0
|
| 118 |
+
path = aligned[0].tolist()
|
| 119 |
+
# first frame of each emitted target token (in order)
|
| 120 |
+
starts = []; prev = self.blank
|
| 121 |
+
for fi, tk in enumerate(path):
|
| 122 |
+
if tk != self.blank and tk != prev:
|
| 123 |
+
starts.append(fi)
|
| 124 |
+
prev = tk
|
| 125 |
+
L = min(len(starts), len(tgt_pi))
|
| 126 |
+
# contiguous span per alignable phone: [start_k, start_{k+1})
|
| 127 |
+
for k in range(L):
|
| 128 |
+
s = starts[k]
|
| 129 |
+
e = starts[k+1] if k+1 < len(starts) else T
|
| 130 |
+
dur[tgt_pi[k]] = max(1.0, float(e - s))
|
| 131 |
+
# leading silence -> give to first alignable phone
|
| 132 |
+
if L >= 1 and starts[0] > 0:
|
| 133 |
+
dur[tgt_pi[0]] += starts[0]
|
| 134 |
+
# non-alignable phones (SP / punct / OOV): small relative value
|
| 135 |
+
anchored = [d for d in dur if d > 0]
|
| 136 |
+
base = (sum(anchored)/len(anchored)) if anchored else 4.0
|
| 137 |
+
for pi in range(n):
|
| 138 |
+
if dur[pi] == 0:
|
| 139 |
+
p = phones[pi]
|
| 140 |
+
dur[pi] = base*0.5 if p == 'SP' else base*0.25
|
| 141 |
+
return [max(1,int(round(x))) for x in dur], len(tgt_pi)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def main():
|
| 145 |
+
ap = argparse.ArgumentParser()
|
| 146 |
+
ap.add_argument("--manifest", required=True)
|
| 147 |
+
ap.add_argument("--out", required=True)
|
| 148 |
+
ap.add_argument("--limit", type=int, default=0)
|
| 149 |
+
ap.add_argument("--device", default="cuda")
|
| 150 |
+
args = ap.parse_args()
|
| 151 |
+
dev = args.device if torch.cuda.is_available() else "cpu"
|
| 152 |
+
run, F = build_frontend_with_charidx()
|
| 153 |
+
al = Aligner(dev)
|
| 154 |
+
rows = [json.loads(l) for l in open(args.manifest)]
|
| 155 |
+
if args.limit: rows = rows[:args.limit]
|
| 156 |
+
out = open(args.out, "w", encoding="utf-8"); n_ok = 0
|
| 157 |
+
for r in rows:
|
| 158 |
+
wav = r.get("wav") or r.get("target_audio")
|
| 159 |
+
try:
|
| 160 |
+
a, sr = sf.read(wav, dtype="float32")
|
| 161 |
+
if a.ndim > 1: a = a.mean(1)
|
| 162 |
+
a16 = librosa.resample(a, orig_sr=sr, target_sr=16000) if sr != 16000 else a
|
| 163 |
+
_, phones, tones, langs = run(r["text"])
|
| 164 |
+
durs, n_anchor = al.durations(a16, phones, langs)
|
| 165 |
+
phone_ids = [F.SYM2ID.get(p, F.SYM2ID['UNK']) for p in phones]
|
| 166 |
+
out.write(json.dumps({
|
| 167 |
+
"id": r["id"], "text": r["text"], "target_audio": wav,
|
| 168 |
+
"phone_ids": phone_ids, "tone_ids": tones, "lang_ids": langs,
|
| 169 |
+
"hifigan_durations": durs, "speaker_id": 0,
|
| 170 |
+
}, ensure_ascii=False)+"\n")
|
| 171 |
+
n_ok += 1
|
| 172 |
+
if n_ok <= 5:
|
| 173 |
+
print(f" {r['id']} n_ph={len(phones)} anchored={n_anchor} "
|
| 174 |
+
f"dur_sum={sum(durs)} dur[:18]={durs[:18]}", flush=True)
|
| 175 |
+
except Exception as e:
|
| 176 |
+
print(f" [skip {r.get('id')}] {type(e).__name__}: {e}", flush=True)
|
| 177 |
+
out.close(); print(f"DONE aligned {n_ok} rows -> {args.out}")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == "__main__":
|
| 181 |
+
main()
|
scripts/assess_big.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""De-noised eval: score a synth dir against eval_big.jsonl (36 held-out sentences).
|
| 3 |
+
Run in moss-nano-venv. Usage: python assess_big.py --synth-dir m7_eval_big
|
| 4 |
+
Pairs with: synth_from_text.py --onnx-dir <m>_onnx --out-dir <m>_eval_big --texts eval_big.jsonl
|
| 5 |
+
Reports aggregate zh CER (zh + mix categories) and en WER, plus per-category, for low-noise comparison."""
|
| 6 |
+
import argparse, json, sys
|
| 7 |
+
ZT = "/home/luigi/jetson-tts/mossnano/zhtw8k"
|
| 8 |
+
sys.path.insert(0, ZT)
|
| 9 |
+
import xasr_offline as X
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
ap = argparse.ArgumentParser()
|
| 14 |
+
ap.add_argument("--synth-dir", required=True)
|
| 15 |
+
ap.add_argument("--tag", default="")
|
| 16 |
+
args = ap.parse_args()
|
| 17 |
+
lang = {r["id"]: r["lang"] for r in (json.loads(l) for l in open(f"{ZT}/eval_big.jsonl"))}
|
| 18 |
+
text = {r["id"]: r["text"] for r in (json.loads(l) for l in open(f"{ZT}/eval_big.jsonl"))}
|
| 19 |
+
rows = [json.loads(l) for l in open(f"{args.synth_dir}/synth.jsonl") if l.strip()]
|
| 20 |
+
cat = {"zh": [], "mix": [], "en": []}
|
| 21 |
+
for r in rows:
|
| 22 |
+
hyp = X.asr(r["wav"])
|
| 23 |
+
sc = X.score(text[r["id"]], hyp)
|
| 24 |
+
v = sc if not isinstance(sc, dict) else sc.get("cer", sc.get("wer"))
|
| 25 |
+
cat[lang[r["id"]]].append(v)
|
| 26 |
+
|
| 27 |
+
def avg(xs):
|
| 28 |
+
return sum(xs) / len(xs) if xs else float("nan")
|
| 29 |
+
zh_cer = avg(cat["zh"] + cat["mix"]) # CER over zh + code-mix
|
| 30 |
+
en_wer = avg(cat["en"])
|
| 31 |
+
print(f"[{args.tag}] N={len(rows)} zh-only={avg(cat['zh']):.3f} mix={avg(cat['mix']):.3f} "
|
| 32 |
+
f"en={avg(cat['en']):.3f}")
|
| 33 |
+
print(f"[{args.tag}] AGGREGATE zh_CER(zh+mix)={zh_cer:.3f} en_WER={en_wer:.3f} "
|
| 34 |
+
f"(n_zh={len(cat['zh'])} n_mix={len(cat['mix'])} n_en={len(cat['en'])})")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
main()
|
scripts/export_8k.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Export a trained zh-TW/en 8k Inflect-Nano (acoustic + snake_8k vocoder) to ONNX.
|
| 3 |
+
Config-driven from the train() checkpoints. FastSpeech split:
|
| 4 |
+
encoder.onnx -> numpy host_regulate -> decoder.onnx -> vocoder.onnx.
|
| 5 |
+
Validates full-pipeline parity vs torch. Run in moss-train-venv."""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import argparse, sys, math, json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import numpy as np, torch
|
| 10 |
+
|
| 11 |
+
REPO = "/tmp/inflect-nano"
|
| 12 |
+
sys.path.insert(0, REPO)
|
| 13 |
+
from inflect_nano.acoustic import MicroFastSpeech, MicroFastSpeechConfig
|
| 14 |
+
from inflect_nano.vocoder import HifiGanGenerator, make_config
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class EncoderHead(torch.nn.Module):
|
| 18 |
+
def __init__(self, m): super().__init__(); self.m = m
|
| 19 |
+
def forward(self, phone, tone, lang, speaker):
|
| 20 |
+
m = self.m
|
| 21 |
+
tok = torch.ones_like(phone, dtype=torch.bool)
|
| 22 |
+
enc = m.encode(phone, tone, lang, speaker, tok)
|
| 23 |
+
log_dur, energy, bright, pitch = m.predict_prosody(enc, tok)
|
| 24 |
+
dur = (torch.exp(log_dur) - 1.0).clamp(0, 80).round().clamp_min(1).long()
|
| 25 |
+
cond = enc + m.energy_proj(energy.unsqueeze(-1)) + m.bright_proj(bright.unsqueeze(-1))
|
| 26 |
+
pitch = torch.stack([pitch[..., 0], pitch[..., 1].clamp(0, 1)], dim=-1)
|
| 27 |
+
return cond, dur, pitch
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class DecoderHead(torch.nn.Module):
|
| 31 |
+
def __init__(self, m): super().__init__(); self.m = m
|
| 32 |
+
def forward(self, frames, frame_meta, local_ctx_raw, abs_pos, pitch_frame, frame_mask):
|
| 33 |
+
m = self.m
|
| 34 |
+
x = frames + m.frame_proj(frame_meta) + m.local_ctx(local_ctx_raw)
|
| 35 |
+
x = x + m.abs_frame(abs_pos)
|
| 36 |
+
if m.cfg.use_frame_pitch:
|
| 37 |
+
x = x + m.pitch_proj(pitch_frame)
|
| 38 |
+
for blk in m.decoder:
|
| 39 |
+
x = blk(x, frame_mask)
|
| 40 |
+
x = x + m.frame_gru(x)[0]
|
| 41 |
+
mel = m.mel_head(x).transpose(1, 2)
|
| 42 |
+
return mel + m.cfg.postnet_scale * m.postnet(mel)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def host_regulate(cond, dur, pitch, abs_bins, max_frames):
|
| 46 |
+
c = cond[0]; d = dur[0].astype(np.int64); d[d < 0] = 0
|
| 47 |
+
T, H = c.shape
|
| 48 |
+
frames = np.repeat(c, d, axis=0); F = frames.shape[0]
|
| 49 |
+
tok = np.repeat(np.arange(T), d); starts = np.cumsum(d) - d
|
| 50 |
+
within = np.arange(F) - starts[tok]; dpf = d[tok].astype(np.float32)
|
| 51 |
+
rel = (within / np.maximum(dpf - 1, 1)).astype(np.float32)
|
| 52 |
+
tc = max(1, int((d > 0).sum())); token_pos = (tok / max(1, tc - 1)).astype(np.float32)
|
| 53 |
+
ld = (np.log1p(dpf) / 6.0).astype(np.float32); center = 1.0 - np.abs(rel * 2 - 1)
|
| 54 |
+
fm = np.stack([rel, 1 - rel, center, np.sin(rel*np.pi), np.cos(rel*np.pi), token_pos, ld, dpf/40.0], -1).astype(np.float32)
|
| 55 |
+
prev = np.concatenate([c[:1], c[:-1]], 0); nxt = np.concatenate([c[1:], c[-1:]], 0)
|
| 56 |
+
lc = np.repeat(np.concatenate([prev, c, nxt], -1), d, axis=0).astype(np.float32)
|
| 57 |
+
pos = np.arange(F); abs_pos = np.minimum(pos*abs_bins//max(1, max_frames), abs_bins-1).astype(np.int64)
|
| 58 |
+
pf = np.repeat(pitch[0], d, axis=0).astype(np.float32)
|
| 59 |
+
return {"frames": frames[None].astype(np.float32), "frame_meta": fm[None], "local_ctx_raw": lc[None],
|
| 60 |
+
"abs_pos": abs_pos[None], "pitch_frame": pf[None], "frame_mask": np.ones((1, F), bool)}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def main():
|
| 64 |
+
ap = argparse.ArgumentParser()
|
| 65 |
+
ap.add_argument("--acoustic-ckpt", required=True)
|
| 66 |
+
ap.add_argument("--vocoder-ckpt", required=True)
|
| 67 |
+
ap.add_argument("--out-dir", required=True)
|
| 68 |
+
ap.add_argument("--symbol-table", default="/home/luigi/jetson-tts/mossnano/zhtw8k/symbol_table.json")
|
| 69 |
+
args = ap.parse_args()
|
| 70 |
+
import onnxruntime as ort
|
| 71 |
+
OUT = Path(args.out_dir); OUT.mkdir(parents=True, exist_ok=True)
|
| 72 |
+
dev = torch.device("cpu")
|
| 73 |
+
|
| 74 |
+
ac = torch.load(args.acoustic_ckpt, map_location=dev, weights_only=False)
|
| 75 |
+
cfg = MicroFastSpeechConfig(**ac["config"])
|
| 76 |
+
m = MicroFastSpeech(cfg); m.load_state_dict(ac["model"]); m.eval()
|
| 77 |
+
enc, dec = EncoderHead(m).eval(), DecoderHead(m).eval()
|
| 78 |
+
print(f"acoustic: sr={cfg.sample_rate} vocab={cfg.vocab_size} tone={cfg.tone_size} lang={cfg.lang_size} "
|
| 79 |
+
f"abs_bins={cfg.abs_frame_bins} max_frames={cfg.max_frames}")
|
| 80 |
+
|
| 81 |
+
vc = torch.load(args.vocoder_ckpt, map_location=dev, weights_only=False)
|
| 82 |
+
vcfg = make_config(vc["config"]["variant"])
|
| 83 |
+
vm = HifiGanGenerator(vcfg); vm.load_state_dict(vc["generator"]); vm.remove_weight_norm(); vm.eval()
|
| 84 |
+
assert vcfg.sample_rate == cfg.sample_rate
|
| 85 |
+
|
| 86 |
+
# sample input: a short valid id sequence (plumbing test)
|
| 87 |
+
T = 40
|
| 88 |
+
g = torch.Generator().manual_seed(0)
|
| 89 |
+
phone = torch.randint(1, min(80, cfg.vocab_size), (1, T), generator=g)
|
| 90 |
+
tone = torch.randint(0, cfg.tone_size, (1, T), generator=g)
|
| 91 |
+
lang = torch.randint(0, cfg.lang_size, (1, T), generator=g)
|
| 92 |
+
spk = torch.zeros(1, dtype=torch.long)
|
| 93 |
+
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
cond, dur, pitch = enc(phone, tone, lang, spk)
|
| 96 |
+
reg = host_regulate(cond.numpy(), dur.numpy(), pitch.numpy(), cfg.abs_frame_bins, cfg.max_frames)
|
| 97 |
+
bt = tuple(torch.from_numpy(reg[k]).clone() for k in ["frames","frame_meta","local_ctx_raw","abs_pos","pitch_frame","frame_mask"])
|
| 98 |
+
mel_split = dec(*bt)
|
| 99 |
+
mel_ref = m.infer(phone, tone, lang, spk)
|
| 100 |
+
print(f"mel parity max_abs_diff={float((mel_ref-mel_split).abs().max()):.2e}")
|
| 101 |
+
|
| 102 |
+
torch.onnx.export(enc, (phone, tone, lang, spk), str(OUT/"acoustic_encoder.onnx"),
|
| 103 |
+
input_names=["phone","tone","lang","speaker"], output_names=["conditioned","durations","pitch"],
|
| 104 |
+
dynamic_axes={"phone":{1:"T"},"tone":{1:"T"},"lang":{1:"T"},"conditioned":{1:"T"},"durations":{1:"T"},"pitch":{1:"T"}},
|
| 105 |
+
opset_version=17, dynamo=False)
|
| 106 |
+
bn = ["frames","frame_meta","local_ctx_raw","abs_pos","pitch_frame","frame_mask"]
|
| 107 |
+
torch.onnx.export(dec, bt, str(OUT/"acoustic_decoder.onnx"),
|
| 108 |
+
input_names=bn, output_names=["mel"],
|
| 109 |
+
dynamic_axes={**{n:{1:"F"} for n in bn}, "mel":{2:"F"}}, opset_version=17, dynamo=False)
|
| 110 |
+
dummy = torch.randn(1, vcfg.num_mels, 60) # match vocoder mel count (40 for snake_8k40, 80 otherwise)
|
| 111 |
+
torch.onnx.export(vm, dummy, str(OUT/"vocoder.onnx"), input_names=["mel"], output_names=["wav"],
|
| 112 |
+
dynamic_axes={"mel":{2:"frames"},"wav":{2:"samples"}}, opset_version=17, dynamo=False)
|
| 113 |
+
|
| 114 |
+
# full-pipeline ONNX parity
|
| 115 |
+
sA = ort.InferenceSession(str(OUT/"acoustic_encoder.onnx"), providers=["CPUExecutionProvider"])
|
| 116 |
+
sB = ort.InferenceSession(str(OUT/"acoustic_decoder.onnx"), providers=["CPUExecutionProvider"])
|
| 117 |
+
sV = ort.InferenceSession(str(OUT/"vocoder.onnx"), providers=["CPUExecutionProvider"])
|
| 118 |
+
oc, od, op = sA.run(None, {"phone":phone.numpy(),"tone":tone.numpy(),"lang":lang.numpy(),"speaker":spk.numpy()})
|
| 119 |
+
reg2 = host_regulate(oc, od, op, cfg.abs_frame_bins, cfg.max_frames)
|
| 120 |
+
feeds = {n:(reg2[n].astype(np.float32) if reg2[n].dtype!=bool else reg2[n]) for n in bn}
|
| 121 |
+
feeds["abs_pos"] = reg2["abs_pos"].astype(np.int64)
|
| 122 |
+
mel_onnx = sB.run(None, feeds)[0]
|
| 123 |
+
wav_onnx = sV.run(None, {"mel": mel_onnx.astype(np.float32)})[0]
|
| 124 |
+
with torch.inference_mode(): wav_ref = vm(mel_ref).numpy()
|
| 125 |
+
n = min(wav_ref.shape[-1], wav_onnx.shape[-1])
|
| 126 |
+
print(f"FULL-PIPELINE wav parity max_abs_diff={float(np.abs(wav_ref[...,:n]-wav_onnx[...,:n]).max()):.2e}")
|
| 127 |
+
# save metadata for the Nano runtime
|
| 128 |
+
json.dump({"sample_rate":cfg.sample_rate,"abs_frame_bins":cfg.abs_frame_bins,"max_frames":cfg.max_frames,
|
| 129 |
+
"hop_size":vcfg.hop_size,"n_mels":cfg.n_mels,"use_frame_pitch":cfg.use_frame_pitch},
|
| 130 |
+
open(OUT/"meta.json","w"), indent=1)
|
| 131 |
+
print("sizes(KB):", {f.name: f.stat().st_size//1024 for f in OUT.glob("*.onnx")})
|
| 132 |
+
print("EXPORT_OK", OUT)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
main()
|
scripts/frontend_bopomofo.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""zh-TW/en unified frontend for the Inflect-Nano retrain.
|
| 2 |
+
zh chars -> bopomofo (g2pw, Taiwan readings) -> zhuyin symbol units + tone (1-5);
|
| 3 |
+
en words -> arpabet (g2p_en) + stress; one sequence, per-phone language id (ZH/EN).
|
| 4 |
+
"""
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
import re
|
| 7 |
+
from g2pw import G2PWConverter
|
| 8 |
+
from g2p_en import G2p
|
| 9 |
+
|
| 10 |
+
# 37 standard zhuyin symbols (U+3105..U+3129)
|
| 11 |
+
ZHUYIN = [chr(c) for c in range(0x3105, 0x312A)]
|
| 12 |
+
ARPABET = ['AA','AE','AH','AO','AW','AY','B','CH','D','DH','EH','ER','EY','F','G','HH',
|
| 13 |
+
'IH','IY','JH','K','L','M','N','NG','OW','OY','P','R','S','SH','T','TH',
|
| 14 |
+
'UH','UW','V','W','Y','Z','ZH']
|
| 15 |
+
PUNCT = [',', '.', '?', '!', 'β¦', '-', "'"]
|
| 16 |
+
SPECIAL = ['_blank', '_pad', 'UNK', 'SP'] # SP = inter-word/space pause
|
| 17 |
+
SYMBOLS = SPECIAL + ZHUYIN + ARPABET + PUNCT
|
| 18 |
+
SYM2ID = {s: i for i, s in enumerate(SYMBOLS)}
|
| 19 |
+
LANG = {'ZH': 0, 'EN': 1} # per-phone language id
|
| 20 |
+
|
| 21 |
+
_g2pw = None
|
| 22 |
+
_g2pen = None
|
| 23 |
+
_zh_num = None
|
| 24 |
+
|
| 25 |
+
_ZH_DIGIT = {"0":"ιΆ","1":"δΈ","2":"δΊ","3":"δΈ","4":"ε","5":"δΊ","6":"ε
","7":"δΈ","8":"ε
«","9":"δΉ"}
|
| 26 |
+
|
| 27 |
+
def _make_zh_normalizer():
|
| 28 |
+
import cn2an
|
| 29 |
+
def norm(text):
|
| 30 |
+
# long digit runs (>=5, e.g. phone/order numbers) -> digit-by-digit zh; else cardinal
|
| 31 |
+
def repl(m):
|
| 32 |
+
d = m.group(0)
|
| 33 |
+
if len(d) >= 5:
|
| 34 |
+
return "".join(_ZH_DIGIT[c] for c in d)
|
| 35 |
+
try:
|
| 36 |
+
return cn2an.an2cn(d, "low")
|
| 37 |
+
except Exception:
|
| 38 |
+
return "".join(_ZH_DIGIT[c] for c in d)
|
| 39 |
+
text = re.sub(r"\d+", repl, text)
|
| 40 |
+
text = text.replace("οΌ", ",").replace("γ", ".").replace("οΌ", "?").replace("οΌ", "!")
|
| 41 |
+
return text
|
| 42 |
+
return norm
|
| 43 |
+
|
| 44 |
+
def _lazy():
|
| 45 |
+
global _g2pw, _g2pen, _zh_num
|
| 46 |
+
if _g2pw is None:
|
| 47 |
+
_g2pw = G2PWConverter()
|
| 48 |
+
_g2pen = G2p()
|
| 49 |
+
_zh_num = _make_zh_normalizer()
|
| 50 |
+
|
| 51 |
+
def _split_syllable(syl: str):
|
| 52 |
+
"""'γγ¨γ’3' -> (['γ','γ¨','γ’'], tone 3)."""
|
| 53 |
+
tone = 0
|
| 54 |
+
if syl and syl[-1].isdigit():
|
| 55 |
+
tone = int(syl[-1]); syl = syl[:-1]
|
| 56 |
+
units = [c for c in syl if c in SYM2ID]
|
| 57 |
+
return units, tone
|
| 58 |
+
|
| 59 |
+
def text_to_phones(text: str):
|
| 60 |
+
_lazy()
|
| 61 |
+
text = _zh_num(text) # numbers->zh words, normalize punct
|
| 62 |
+
bopo = _g2pw(text)[0] # per-char bopomofo or None
|
| 63 |
+
chars = list(text)
|
| 64 |
+
phones, tones, langs = [], [], []
|
| 65 |
+
i = 0
|
| 66 |
+
while i < len(chars):
|
| 67 |
+
b = bopo[i] if i < len(bopo) else None
|
| 68 |
+
ch = chars[i]
|
| 69 |
+
if b is not None: # zh char
|
| 70 |
+
units, tone = _split_syllable(b)
|
| 71 |
+
for u in units:
|
| 72 |
+
phones.append(u); tones.append(min(tone, 5)); langs.append(LANG['ZH'])
|
| 73 |
+
i += 1
|
| 74 |
+
elif re.match(r'[A-Za-z]', ch): # English run -> g2p_en
|
| 75 |
+
j = i
|
| 76 |
+
while j < len(chars) and re.match(r"[A-Za-z']", chars[j]):
|
| 77 |
+
j += 1
|
| 78 |
+
word = ''.join(chars[i:j])
|
| 79 |
+
for p in _g2pen(word):
|
| 80 |
+
p = p.strip()
|
| 81 |
+
if not p:
|
| 82 |
+
continue
|
| 83 |
+
stress = 0
|
| 84 |
+
if p[-1].isdigit():
|
| 85 |
+
stress = int(p[-1]); p = p[:-1]
|
| 86 |
+
if p in SYM2ID:
|
| 87 |
+
phones.append(p); tones.append(stress); langs.append(LANG['EN'])
|
| 88 |
+
phones.append('SP'); tones.append(0); langs.append(LANG['EN'])
|
| 89 |
+
i = j
|
| 90 |
+
else: # punctuation / space / other
|
| 91 |
+
if ch in PUNCT:
|
| 92 |
+
phones.append(ch); tones.append(0); langs.append(LANG['ZH'])
|
| 93 |
+
elif ch.strip() == '':
|
| 94 |
+
if phones and phones[-1] != 'SP':
|
| 95 |
+
phones.append('SP'); tones.append(0); langs.append(LANG['ZH'])
|
| 96 |
+
i += 1
|
| 97 |
+
return phones, tones, langs
|
| 98 |
+
|
| 99 |
+
def text_to_ids(text: str):
|
| 100 |
+
phones, tones, langs = text_to_phones(text)
|
| 101 |
+
ids = [SYM2ID.get(p, SYM2ID['UNK']) for p in phones]
|
| 102 |
+
return {"phones": phones, "phone_ids": ids, "tone_ids": tones, "lang_ids": langs}
|
scripts/gen_breezy_corpus.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate the BreezyVoice teacher corpus for distillation into Inflect-Nano.
|
| 3 |
+
Clean short reference (no prompt-leak) + INLINE ASR GATE: each clip is transcribed
|
| 4 |
+
(faster-whisper) and kept only if Han-CER vs intended text is below threshold; else
|
| 5 |
+
retried, then skipped. Writes 22.05kHz wav + manifest {id,text,wav,dur,cer}. Resumable.
|
| 6 |
+
Run in .venv-breezy with PYTHONPATH=BreezyVoice repo.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
import argparse, json, os, sys, time
|
| 10 |
+
import soundfile as sf, numpy as np
|
| 11 |
+
|
| 12 |
+
BV = "/home/luigi/jetson-tts/third_party/BreezyVoice"
|
| 13 |
+
ZT = "/home/luigi/jetson-tts/mossnano/zhtw8k"
|
| 14 |
+
sys.path.insert(0, BV)
|
| 15 |
+
from single_inference import CustomCosyVoice, get_bopomofo_rare
|
| 16 |
+
from g2pw import G2PWConverter
|
| 17 |
+
from cosyvoice.utils.file_utils import load_wav
|
| 18 |
+
from faster_whisper import WhisperModel
|
| 19 |
+
|
| 20 |
+
REF_AUDIO = f"{ZT}/ref/ref_clean.wav"
|
| 21 |
+
REF_TEXT = open(f"{ZT}/ref/ref_clean.txt").read().strip()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
import re as _re
|
| 25 |
+
import opencc as _opencc
|
| 26 |
+
_T2S = _opencc.OpenCC('t2s') # normalize trad<->simp so the gate scores REAL pronunciation, not script
|
| 27 |
+
def han(s): return "".join(c for c in s if "δΈ" <= c <= "ιΏΏ")
|
| 28 |
+
def is_zh(text): return bool(_re.search(r"[δΈ-ιΏΏ]", text))
|
| 29 |
+
def _lev(r, h):
|
| 30 |
+
if not r: return 0.0
|
| 31 |
+
d = list(range(len(h)+1))
|
| 32 |
+
for i in range(1, len(r)+1):
|
| 33 |
+
prev = d[0]; d[0] = i
|
| 34 |
+
for j in range(1, len(h)+1):
|
| 35 |
+
cur = d[j]; d[j] = min(d[j]+1, d[j-1]+1, prev+(r[i-1] != h[j-1])); prev = cur
|
| 36 |
+
return d[len(h)]/len(r)
|
| 37 |
+
def _enwords(s): return _re.findall(r"[a-z']+", s.lower())
|
| 38 |
+
def score(ref, hyp):
|
| 39 |
+
"""Han-CER for zh/mix; word-error-rate for pure-English. Lower = better."""
|
| 40 |
+
if is_zh(ref):
|
| 41 |
+
return _lev(han(_T2S.convert(ref)), han(_T2S.convert(hyp)))
|
| 42 |
+
return _lev(_enwords(ref), _enwords(hyp)) # word-level Levenshtein ratio
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def read_tsv(path, limit, skip):
|
| 46 |
+
rows = []
|
| 47 |
+
for line in open(path, encoding="utf-8"):
|
| 48 |
+
line = line.rstrip("\n")
|
| 49 |
+
if not line.strip(): continue
|
| 50 |
+
parts = line.split("\t")
|
| 51 |
+
rows.append((parts[0] if len(parts) > 1 else f"utt{len(rows):06d}", parts[-1].strip()))
|
| 52 |
+
rows = rows[skip:]
|
| 53 |
+
return rows[:limit] if limit else rows
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def main():
|
| 57 |
+
ap = argparse.ArgumentParser()
|
| 58 |
+
ap.add_argument("--model", default="/home/luigi/jetson-tts/models/BreezyVoice")
|
| 59 |
+
ap.add_argument("--corpus", default="/home/luigi/jetson-tts/data/text/train.tsv")
|
| 60 |
+
ap.add_argument("--out-dir", required=True)
|
| 61 |
+
ap.add_argument("--limit", type=int, default=0)
|
| 62 |
+
ap.add_argument("--skip", type=int, default=0)
|
| 63 |
+
ap.add_argument("--min-sec", type=float, default=0.8)
|
| 64 |
+
ap.add_argument("--max-sec", type=float, default=20.0)
|
| 65 |
+
ap.add_argument("--cer-thresh", type=float, default=0.30)
|
| 66 |
+
ap.add_argument("--retries", type=int, default=3)
|
| 67 |
+
args = ap.parse_args()
|
| 68 |
+
|
| 69 |
+
os.makedirs(args.out_dir, exist_ok=True)
|
| 70 |
+
man_path = os.path.join(args.out_dir, "manifest.jsonl")
|
| 71 |
+
done = set()
|
| 72 |
+
if os.path.exists(man_path):
|
| 73 |
+
for l in open(man_path):
|
| 74 |
+
try: done.add(json.loads(l)["id"])
|
| 75 |
+
except Exception: pass
|
| 76 |
+
print(f"resuming: {len(done)} done | ref: {REF_TEXT}")
|
| 77 |
+
|
| 78 |
+
cv = CustomCosyVoice(args.model); conv = G2PWConverter()
|
| 79 |
+
asr = WhisperModel("SoybeanMilk/faster-whisper-Breeze-ASR-25", device="cuda", compute_type="float16") # zh-TW gate (traditional output)
|
| 80 |
+
ref_bopo = get_bopomofo_rare(cv.frontend.text_normalize_new(REF_TEXT, split=False), conv)
|
| 81 |
+
ref_wav = load_wav(REF_AUDIO, 16000)
|
| 82 |
+
|
| 83 |
+
rows = read_tsv(args.corpus, args.limit, args.skip)
|
| 84 |
+
todo = [(u, t) for u, t in rows if u not in done]
|
| 85 |
+
print(f"to synth: {len(todo)} / {len(rows)}")
|
| 86 |
+
mf = open(man_path, "a", encoding="utf-8")
|
| 87 |
+
t0 = time.time(); n_ok = 0; n_skip = 0; tot = 0.0
|
| 88 |
+
for i, (utt, text) in enumerate(todo):
|
| 89 |
+
bopo = get_bopomofo_rare(cv.frontend.text_normalize_new(text, split=False), conv)
|
| 90 |
+
best = None; best_cer = 9.9
|
| 91 |
+
for attempt in range(args.retries):
|
| 92 |
+
try:
|
| 93 |
+
out = cv.inference_zero_shot_no_normalize(bopo, ref_bopo, ref_wav)
|
| 94 |
+
w = out["tts_speech"].squeeze().cpu().numpy().astype(np.float32)
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f" [err {utt}] {e}"); continue
|
| 97 |
+
dur = len(w) / 22050
|
| 98 |
+
if dur < args.min_sec or dur > args.max_sec: continue
|
| 99 |
+
tmp = os.path.join(args.out_dir, f".{utt}.tmp.wav"); sf.write(tmp, w, 22050)
|
| 100 |
+
segs, _ = asr.transcribe(tmp, language=("zh" if is_zh(text) else "en"), beam_size=5)
|
| 101 |
+
c = score(text, "".join(s.text for s in segs))
|
| 102 |
+
if c < best_cer: best_cer = c; best = (w, dur, tmp)
|
| 103 |
+
if c <= args.cer_thresh: break
|
| 104 |
+
if best is None or best_cer > args.cer_thresh:
|
| 105 |
+
n_skip += 1
|
| 106 |
+
if best and os.path.exists(best[2]): os.remove(best[2])
|
| 107 |
+
if n_skip <= 20 or n_skip % 50 == 0:
|
| 108 |
+
print(f" [SKIP {utt}] best_cer={best_cer:.2f} | {text[:30]}")
|
| 109 |
+
continue
|
| 110 |
+
w, dur, tmp = best
|
| 111 |
+
wp = os.path.join(args.out_dir, f"{utt}.wav"); os.replace(tmp, wp)
|
| 112 |
+
mf.write(json.dumps({"id": utt, "text": text, "wav": wp, "dur": round(dur, 3),
|
| 113 |
+
"cer": round(best_cer, 3)}, ensure_ascii=False) + "\n"); mf.flush()
|
| 114 |
+
n_ok += 1; tot += dur
|
| 115 |
+
if (i + 1) % 25 == 0:
|
| 116 |
+
el = time.time() - t0
|
| 117 |
+
print(f" {i+1}/{len(todo)} ok={n_ok} skip={n_skip} audio={tot/60:.1f}min "
|
| 118 |
+
f"{el/(i+1):.2f}s/clip eta={(len(todo)-i-1)*el/(i+1)/60:.0f}min")
|
| 119 |
+
mf.close()
|
| 120 |
+
print(f"DONE ok={n_ok} skip={n_skip} audio={tot/60:.1f}min in {(time.time()-t0)/60:.0f}min")
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
main()
|
scripts/probe_forced_synth.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""RANK-1 acoustic probe: synth N training clips with FORCED (ground-truth) durations through an
|
| 3 |
+
ONNX dir, isolating the phones->mel mapping from the duration predictor + g2p frontend.
|
| 4 |
+
Reads phone/tone/lang ids + GT durations directly from m2_align.jsonl (no frontend).
|
| 5 |
+
Run in moss-train-venv. Then ASR the wavs (probe_forced_asr via xasr_offline) -> CER.
|
| 6 |
+
Pairs the 0.80(forced)-vs-0.145(GT-mel) acoustic gap to a single number per config."""
|
| 7 |
+
import argparse, json, sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import numpy as np, soundfile as sf, onnxruntime as ort
|
| 10 |
+
ZT = "/home/luigi/jetson-tts/mossnano/zhtw8k"
|
| 11 |
+
sys.path.insert(0, ZT)
|
| 12 |
+
from synth_from_text import host_regulate
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
ap = argparse.ArgumentParser()
|
| 17 |
+
ap.add_argument("--onnx-dir", required=True)
|
| 18 |
+
ap.add_argument("--out-dir", required=True)
|
| 19 |
+
ap.add_argument("--n", type=int, default=30)
|
| 20 |
+
ap.add_argument("--align-jsonl", default=f"{ZT}/m2_align.jsonl")
|
| 21 |
+
args = ap.parse_args()
|
| 22 |
+
meta = json.load(open(f"{args.onnx_dir}/meta.json"))
|
| 23 |
+
so = ort.SessionOptions(); so.intra_op_num_threads = 4
|
| 24 |
+
sA = ort.InferenceSession(f"{args.onnx_dir}/acoustic_encoder.onnx", so, providers=["CPUExecutionProvider"])
|
| 25 |
+
sB = ort.InferenceSession(f"{args.onnx_dir}/acoustic_decoder.onnx", so, providers=["CPUExecutionProvider"])
|
| 26 |
+
sV = ort.InferenceSession(f"{args.onnx_dir}/vocoder.onnx", so, providers=["CPUExecutionProvider"])
|
| 27 |
+
bn = ["frames", "frame_meta", "local_ctx_raw", "abs_pos", "pitch_frame", "frame_mask"]
|
| 28 |
+
Path(args.out_dir).mkdir(parents=True, exist_ok=True)
|
| 29 |
+
rows = [json.loads(l) for l in open(args.align_jsonl) if l.strip()][:args.n]
|
| 30 |
+
out = open(f"{args.out_dir}/synth.jsonl", "w")
|
| 31 |
+
for i, r in enumerate(rows):
|
| 32 |
+
phone = np.array([r["phone_ids"]], np.int64); tone = np.array([r["tone_ids"]], np.int64)
|
| 33 |
+
lang = np.array([r["lang_ids"]], np.int64); spk = np.zeros(1, np.int64)
|
| 34 |
+
cond, _dur_pred, pitch = sA.run(None, {"phone": phone, "tone": tone, "lang": lang, "speaker": spk})
|
| 35 |
+
# substitute GT (forced) durations, rescaled so total ~ predicted length (stable regulator)
|
| 36 |
+
df = np.array([r["hifigan_durations"]], np.float32)
|
| 37 |
+
df = df * (_dur_pred.sum() / max(1.0, df.sum()))
|
| 38 |
+
reg = host_regulate(cond, df, pitch, meta["abs_frame_bins"], meta["max_frames"])
|
| 39 |
+
feeds = {n: (reg[n].astype(np.float32) if reg[n].dtype != bool else reg[n]) for n in bn}
|
| 40 |
+
feeds["abs_pos"] = reg["abs_pos"].astype(np.int64)
|
| 41 |
+
mel = sB.run(None, feeds)[0]
|
| 42 |
+
wav = sV.run(None, {"mel": mel.astype(np.float32)})[0].reshape(-1)
|
| 43 |
+
wp = f"{args.out_dir}/p{i:03d}.wav"; sf.write(wp, wav, meta["sample_rate"])
|
| 44 |
+
out.write(json.dumps({"id": f"p{i:03d}", "text": r["text"], "wav": wp}, ensure_ascii=False) + "\n")
|
| 45 |
+
out.close()
|
| 46 |
+
print(f"PROBE SYNTH DONE {len(rows)} clips -> {args.out_dir}/synth.jsonl")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
scripts/run_bilingual.sh
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# BILINGUAL NATIVE experiment: apply the proven recipe (v4 phone-align + diverse data) symmetrically.
|
| 3 |
+
# Merge zh-expanded corpus (15423, mark voice, incl code-mix) + diverse English-expanded (mark voice)
|
| 4 |
+
# -> one balanced single-voice bilingual corpus -> train ONE 4.63M model good at zh AND en (+code-mix).
|
| 5 |
+
# Run when English synth done + a GPU free. GPU via $1 (default 0).
|
| 6 |
+
set -e
|
| 7 |
+
ZT=/home/luigi/jetson-tts/mossnano/zhtw8k; LOG=$ZT/bilingual.log
|
| 8 |
+
TRAIN=/home/luigi/moss-train-venv/bin/python
|
| 9 |
+
GPU=${1:-0}
|
| 10 |
+
VOC=$ZT/m2_vocoder_8k/hifigan-snake_8k-latest.pt
|
| 11 |
+
exec >>"$LOG" 2>&1
|
| 12 |
+
echo "===== BILINGUAL START $(date) GPU=$GPU ====="
|
| 13 |
+
|
| 14 |
+
# 1) canonical-fix English-expand manifest wav paths from id
|
| 15 |
+
$TRAIN - <<PY
|
| 16 |
+
import json, os
|
| 17 |
+
p="$ZT/teacher_corpus_en_expand/manifest.jsonl"; D="$ZT/teacher_corpus_en_expand"
|
| 18 |
+
rows=[json.loads(l) for l in open(p) if l.strip()]
|
| 19 |
+
ok=0
|
| 20 |
+
for r in rows: r["wav"]=f"{D}/{r['id']}.wav"; ok+=os.path.exists(r["wav"])
|
| 21 |
+
open(p,"w").write("\n".join(json.dumps(r,ensure_ascii=False) for r in rows)+"\n")
|
| 22 |
+
print(f"[en-expand manifest] {len(rows)} rows, {ok} files exist")
|
| 23 |
+
PY
|
| 24 |
+
|
| 25 |
+
# 2) v4 phone-align the English-expand clips (espeak handles English natively)
|
| 26 |
+
echo "[align en-expand] $(date)"
|
| 27 |
+
CUDA_VISIBLE_DEVICES=$GPU $TRAIN $ZT/align_durations_v4.py \
|
| 28 |
+
--manifest $ZT/teacher_corpus_en_expand/manifest.jsonl \
|
| 29 |
+
--out $ZT/en_expand_v4_align.jsonl --device cuda
|
| 30 |
+
|
| 31 |
+
# 3) merge zh-expanded + en-expanded -> bilingual corpus (all mark voice, speaker 0)
|
| 32 |
+
$TRAIN - <<PY
|
| 33 |
+
import json
|
| 34 |
+
def fix(p):
|
| 35 |
+
rows=[json.loads(l) for l in open(p) if l.strip()]
|
| 36 |
+
for r in rows:
|
| 37 |
+
if not r["target_audio"].startswith("/"): r["target_audio"]="/home/luigi/jetson-tts/"+r["target_audio"]
|
| 38 |
+
return rows
|
| 39 |
+
a=fix("$ZT/m_v4_v2_align.jsonl"); b=fix("$ZT/en_expand_v4_align.jsonl")
|
| 40 |
+
out="$ZT/m_bilingual_align.jsonl"
|
| 41 |
+
open(out,"w").write("\n".join(json.dumps(r,ensure_ascii=False) for r in a+b)+"\n")
|
| 42 |
+
import re
|
| 43 |
+
en=sum(1 for r in a+b if all(ord(c)<128 for c in r["text"]))
|
| 44 |
+
print(f"[bilingual] {len(a)} + {len(b)} = {len(a)+len(b)} rows -> {out} | ~{en} pure-ASCII(en) rows")
|
| 45 |
+
PY
|
| 46 |
+
|
| 47 |
+
# 4) train the bilingual model (en-upsample 1: English now abundant via expansion)
|
| 48 |
+
echo "[bilingual acoustic] $(date)"
|
| 49 |
+
cd /tmp/inflect-nano
|
| 50 |
+
CUDA_VISIBLE_DEVICES=$GPU $TRAIN -m inflect_nano.acoustic --durations-jsonl $ZT/m_bilingual_align.jsonl \
|
| 51 |
+
--out-dir $ZT/bili_acoustic_8k --vocoder-variant snake_8k --sample-rate 8000 \
|
| 52 |
+
--steps 60000 --batch-size 16 --lr 2e-4 --max-frames 1000 --en-upsample 1 \
|
| 53 |
+
--vocoder-checkpoint $VOC --vocoder-mel-weight 1.0 \
|
| 54 |
+
--save-interval 5000 --log-interval 200 --device cuda
|
| 55 |
+
echo "===== BILINGUAL TRAIN DONE $(date) ====="
|
scripts/run_v4zh_v2.sh
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# v4zh-v2: retrain on DIVERSITY-EXPANDED corpus (existing 9042 + ~6500 broad-char Tatoeba/zh-TW).
|
| 3 |
+
# Tests the data-diversity hypothesis: held-out zh should drop from 0.40 toward the 0.21 seen-text floor.
|
| 4 |
+
# Keeps the proven v4 phone-level alignment. Run when teacher synth + GPU are free. GPU via $1 (default 1).
|
| 5 |
+
set -e
|
| 6 |
+
ZT=/home/luigi/jetson-tts/mossnano/zhtw8k; LOG=$ZT/v4zh_v2.log
|
| 7 |
+
TRAIN=/home/luigi/moss-train-venv/bin/python
|
| 8 |
+
GPU=${1:-1}
|
| 9 |
+
VOC=$ZT/m2_vocoder_8k/hifigan-snake_8k-latest.pt
|
| 10 |
+
exec >>"$LOG" 2>&1
|
| 11 |
+
echo "===== V4ZH_V2 START $(date) GPU=$GPU ====="
|
| 12 |
+
|
| 13 |
+
# 1) canonical-fix expansion manifest wav paths from id (idempotent, no doubling)
|
| 14 |
+
$TRAIN - <<PY
|
| 15 |
+
import json, os
|
| 16 |
+
p="$ZT/teacher_corpus_expand/manifest.jsonl"; D="$ZT/teacher_corpus_expand"
|
| 17 |
+
rows=[json.loads(l) for l in open(p) if l.strip()]
|
| 18 |
+
ok=0
|
| 19 |
+
for r in rows:
|
| 20 |
+
r["wav"]=f"{D}/{r['id']}.wav"; ok+=os.path.exists(r["wav"])
|
| 21 |
+
open(p,"w").write("\n".join(json.dumps(r,ensure_ascii=False) for r in rows)+"\n")
|
| 22 |
+
print(f"[expand manifest] {len(rows)} rows, {ok} files exist")
|
| 23 |
+
PY
|
| 24 |
+
|
| 25 |
+
# 2) v4-align the expansion clips (phone-level)
|
| 26 |
+
echo "[align expand] $(date)"
|
| 27 |
+
CUDA_VISIBLE_DEVICES=$GPU $TRAIN $ZT/align_durations_v4.py \
|
| 28 |
+
--manifest $ZT/teacher_corpus_expand/manifest.jsonl \
|
| 29 |
+
--out $ZT/m_v4_expand_align.jsonl --device cuda
|
| 30 |
+
|
| 31 |
+
# 3) abspath-fix + merge with existing v4 alignment
|
| 32 |
+
$TRAIN - <<PY
|
| 33 |
+
import json
|
| 34 |
+
R="/home/luigi/jetson-tts/"
|
| 35 |
+
def fix(p):
|
| 36 |
+
rows=[json.loads(l) for l in open(p) if l.strip()]
|
| 37 |
+
for r in rows:
|
| 38 |
+
if not r["target_audio"].startswith("/"): r["target_audio"]=R+r["target_audio"]
|
| 39 |
+
return rows
|
| 40 |
+
a=fix("$ZT/m_v4_align.jsonl"); b=fix("$ZT/m_v4_expand_align.jsonl")
|
| 41 |
+
out="$ZT/m_v4_v2_align.jsonl"
|
| 42 |
+
open(out,"w").write("\n".join(json.dumps(r,ensure_ascii=False) for r in a+b)+"\n")
|
| 43 |
+
print(f"[merged] {len(a)} + {len(b)} = {len(a)+len(b)} -> {out}")
|
| 44 |
+
PY
|
| 45 |
+
|
| 46 |
+
# 4) retrain on merged corpus (same recipe, frozen arch)
|
| 47 |
+
echo "[v4zh-v2 acoustic] $(date)"
|
| 48 |
+
cd /tmp/inflect-nano
|
| 49 |
+
CUDA_VISIBLE_DEVICES=$GPU $TRAIN -m inflect_nano.acoustic --durations-jsonl $ZT/m_v4_v2_align.jsonl \
|
| 50 |
+
--out-dir $ZT/v4zh_v2_acoustic_8k --vocoder-variant snake_8k --sample-rate 8000 \
|
| 51 |
+
--steps 60000 --batch-size 16 --lr 2e-4 --max-frames 1000 --en-upsample 2 \
|
| 52 |
+
--vocoder-checkpoint $VOC --vocoder-mel-weight 1.0 \
|
| 53 |
+
--save-interval 5000 --log-interval 200 --device cuda
|
| 54 |
+
echo "===== V4ZH_V2 TRAIN DONE $(date) ====="
|
scripts/run_voc_retrain.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Retrain the 8k Snake-HiFiGAN vocoder on the FULL diverse corpus (~21k, zh+en mark voice),
|
| 3 |
+
# longer + higher MR-STFT weight -> crisper, less-buzzy 8kHz audio (raises the GT-mel->audio ceiling).
|
| 4 |
+
set -e
|
| 5 |
+
ZT=/home/luigi/jetson-tts/mossnano/zhtw8k; LOG=$ZT/voc_retrain.log
|
| 6 |
+
TRAIN=/home/luigi/moss-train-venv/bin/python
|
| 7 |
+
exec >>"$LOG" 2>&1
|
| 8 |
+
echo "===== VOC RETRAIN START $(date) ====="
|
| 9 |
+
cd /tmp/inflect-nano
|
| 10 |
+
CUDA_VISIBLE_DEVICES=1 $TRAIN -m inflect_nano.vocoder --train-jsonl $ZT/bili_voc_rows.jsonl \
|
| 11 |
+
--out-dir $ZT/bili_vocoder_8k --variant snake_8k --steps 40000 --batch-size 16 \
|
| 12 |
+
--segment-size 8192 --min-seconds 0.8 --max-seconds 20 --num-workers 4 --stft-weight 2.5 \
|
| 13 |
+
--save-interval 5000 --log-interval 200 --device cuda
|
| 14 |
+
echo "===== VOC RETRAIN DONE $(date) ====="
|
scripts/select_diverse_text.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Select a DIVERSE training-text set to maximize phoneme/character coverage.
|
| 3 |
+
|
| 4 |
+
Why this matters: at 4.63M params the model is NOT capacity-limited β but it can only
|
| 5 |
+
pronounce characters/words it has SEEN. A narrow corpus (e.g. a few hundred Han chars)
|
| 6 |
+
leaves most held-out characters unseen -> garbled output. This script builds a broad,
|
| 7 |
+
coverage-maximizing text set from Tatoeba so the teacher (and then the student) cover
|
| 8 |
+
the common vocabulary.
|
| 9 |
+
|
| 10 |
+
zh-TW: Tatoeba `cmn` -> OpenCC s2twp (Taiwan traditional) -> greedy Han-CHAR coverage.
|
| 11 |
+
en : Tatoeba `eng` -> greedy WORD coverage (English phones are few; word/prosody variety matters).
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
python select_diverse_text.py --lang zh --n 6000 --out expand_zh.tsv
|
| 15 |
+
python select_diverse_text.py --lang en --n 6000 --out expand_en.tsv
|
| 16 |
+
Then feed the .tsv (id<TAB>text) to gen_breezy_corpus.py to synthesize the teacher audio.
|
| 17 |
+
|
| 18 |
+
Deps: requests/urllib (download), opencc (zh only). Tatoeba dumps are CC-BY 2.0 FR.
|
| 19 |
+
"""
|
| 20 |
+
import argparse, bz2, os, re, random, urllib.request
|
| 21 |
+
|
| 22 |
+
TATOEBA = "https://downloads.tatoeba.org/exports/per_language/{lang}/{lang}_sentences.tsv.bz2"
|
| 23 |
+
HAN = lambda s: set(c for c in s if "δΈ" <= c <= "ιΏΏ")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def download(lang):
|
| 27 |
+
f = f"{lang}_sentences.tsv"
|
| 28 |
+
if not os.path.exists(f):
|
| 29 |
+
url = TATOEBA.format(lang=lang)
|
| 30 |
+
print("downloading", url)
|
| 31 |
+
urllib.request.urlretrieve(url, f + ".bz2")
|
| 32 |
+
with bz2.open(f + ".bz2", "rt", encoding="utf-8") as i, open(f, "w", encoding="utf-8") as o:
|
| 33 |
+
for line in i:
|
| 34 |
+
o.write(line)
|
| 35 |
+
return f
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def select_zh(path, n, seed=42):
|
| 39 |
+
import opencc
|
| 40 |
+
cc = opencc.OpenCC("s2twp") # simplified -> Taiwan traditional (with phrase conversion)
|
| 41 |
+
allowed = set("οΌγοΌοΌγοΌοΌβ¦")
|
| 42 |
+
seen_t, cands = set(), []
|
| 43 |
+
for l in open(path, encoding="utf-8"):
|
| 44 |
+
p = l.rstrip("\n").split("\t")
|
| 45 |
+
if len(p) < 3:
|
| 46 |
+
continue
|
| 47 |
+
t = cc.convert(p[2].strip()).replace(",", "οΌ").replace("!", "οΌ").replace("?", "οΌ")
|
| 48 |
+
h = HAN(t)
|
| 49 |
+
if not (6 <= len(h) <= 26):
|
| 50 |
+
continue
|
| 51 |
+
if any(("δΈ" <= c <= "ιΏΏ") is False and c not in allowed for c in t):
|
| 52 |
+
continue
|
| 53 |
+
if t not in seen_t:
|
| 54 |
+
seen_t.add(t); cands.append(t)
|
| 55 |
+
return greedy_cover(cands, HAN, n, seed)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def select_en(path, n, seed=42):
|
| 59 |
+
words = lambda s: set(re.findall(r"[a-z']+", s.lower()))
|
| 60 |
+
seen_t, cands = set(), []
|
| 61 |
+
for l in open(path, encoding="utf-8"):
|
| 62 |
+
p = l.rstrip("\n").split("\t")
|
| 63 |
+
if len(p) < 3:
|
| 64 |
+
continue
|
| 65 |
+
t = p[2].strip()
|
| 66 |
+
if not re.fullmatch(r"[A-Za-z0-9 ,.\-'?!]+", t):
|
| 67 |
+
continue
|
| 68 |
+
w = re.findall(r"[A-Za-z']+", t)
|
| 69 |
+
if not (4 <= len(w) <= 14) or any(len(x) > 15 for x in w):
|
| 70 |
+
continue
|
| 71 |
+
if t not in seen_t:
|
| 72 |
+
seen_t.add(t); cands.append(t)
|
| 73 |
+
return greedy_cover(cands, words, n, seed)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def greedy_cover(cands, unit, n, seed):
|
| 77 |
+
"""Greedy max-coverage of `unit(text)` items, then random top-up to n for frequency."""
|
| 78 |
+
random.seed(seed); random.shuffle(cands)
|
| 79 |
+
covered, selected, rest = set(), [], []
|
| 80 |
+
cands.sort(key=lambda t: len(unit(t) - covered), reverse=True)
|
| 81 |
+
for t in cands:
|
| 82 |
+
if len(unit(t) - covered) >= 1 and len(selected) < n:
|
| 83 |
+
selected.append(t); covered |= unit(t)
|
| 84 |
+
else:
|
| 85 |
+
rest.append(t)
|
| 86 |
+
random.shuffle(rest)
|
| 87 |
+
selected += rest[: max(0, n - len(selected))]
|
| 88 |
+
print(f"selected {len(selected)} sentences | unique units covered: {len(covered)}")
|
| 89 |
+
return selected
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def main():
|
| 93 |
+
ap = argparse.ArgumentParser()
|
| 94 |
+
ap.add_argument("--lang", choices=["zh", "en"], required=True)
|
| 95 |
+
ap.add_argument("--n", type=int, default=6000)
|
| 96 |
+
ap.add_argument("--out", required=True)
|
| 97 |
+
args = ap.parse_args()
|
| 98 |
+
path = download("cmn" if args.lang == "zh" else "eng")
|
| 99 |
+
sents = select_zh(path, args.n) if args.lang == "zh" else select_en(path, args.n)
|
| 100 |
+
with open(args.out, "w", encoding="utf-8") as o:
|
| 101 |
+
for i, t in enumerate(sents):
|
| 102 |
+
o.write(f"{args.lang}e{i:05d}\t{t}\n")
|
| 103 |
+
print("wrote", args.out, len(sents))
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
main()
|
scripts/symbol_table.json
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"symbols": [
|
| 3 |
+
"_blank",
|
| 4 |
+
"_pad",
|
| 5 |
+
"UNK",
|
| 6 |
+
"SP",
|
| 7 |
+
"γ
",
|
| 8 |
+
"γ",
|
| 9 |
+
"γ",
|
| 10 |
+
"γ",
|
| 11 |
+
"γ",
|
| 12 |
+
"γ",
|
| 13 |
+
"γ",
|
| 14 |
+
"γ",
|
| 15 |
+
"γ",
|
| 16 |
+
"γ",
|
| 17 |
+
"γ",
|
| 18 |
+
"γ",
|
| 19 |
+
"γ",
|
| 20 |
+
"γ",
|
| 21 |
+
"γ",
|
| 22 |
+
"γ",
|
| 23 |
+
"γ",
|
| 24 |
+
"γ",
|
| 25 |
+
"γ",
|
| 26 |
+
"γ",
|
| 27 |
+
"γ",
|
| 28 |
+
"γ",
|
| 29 |
+
"γ",
|
| 30 |
+
"γ",
|
| 31 |
+
"γ",
|
| 32 |
+
"γ",
|
| 33 |
+
"γ",
|
| 34 |
+
"γ ",
|
| 35 |
+
"γ‘",
|
| 36 |
+
"γ’",
|
| 37 |
+
"γ£",
|
| 38 |
+
"γ€",
|
| 39 |
+
"γ₯",
|
| 40 |
+
"γ¦",
|
| 41 |
+
"γ§",
|
| 42 |
+
"γ¨",
|
| 43 |
+
"γ©",
|
| 44 |
+
"AA",
|
| 45 |
+
"AE",
|
| 46 |
+
"AH",
|
| 47 |
+
"AO",
|
| 48 |
+
"AW",
|
| 49 |
+
"AY",
|
| 50 |
+
"B",
|
| 51 |
+
"CH",
|
| 52 |
+
"D",
|
| 53 |
+
"DH",
|
| 54 |
+
"EH",
|
| 55 |
+
"ER",
|
| 56 |
+
"EY",
|
| 57 |
+
"F",
|
| 58 |
+
"G",
|
| 59 |
+
"HH",
|
| 60 |
+
"IH",
|
| 61 |
+
"IY",
|
| 62 |
+
"JH",
|
| 63 |
+
"K",
|
| 64 |
+
"L",
|
| 65 |
+
"M",
|
| 66 |
+
"N",
|
| 67 |
+
"NG",
|
| 68 |
+
"OW",
|
| 69 |
+
"OY",
|
| 70 |
+
"P",
|
| 71 |
+
"R",
|
| 72 |
+
"S",
|
| 73 |
+
"SH",
|
| 74 |
+
"T",
|
| 75 |
+
"TH",
|
| 76 |
+
"UH",
|
| 77 |
+
"UW",
|
| 78 |
+
"V",
|
| 79 |
+
"W",
|
| 80 |
+
"Y",
|
| 81 |
+
"Z",
|
| 82 |
+
"ZH",
|
| 83 |
+
",",
|
| 84 |
+
".",
|
| 85 |
+
"?",
|
| 86 |
+
"!",
|
| 87 |
+
"β¦",
|
| 88 |
+
"-",
|
| 89 |
+
"'"
|
| 90 |
+
],
|
| 91 |
+
"sym2id": {
|
| 92 |
+
"_blank": 0,
|
| 93 |
+
"_pad": 1,
|
| 94 |
+
"UNK": 2,
|
| 95 |
+
"SP": 3,
|
| 96 |
+
"γ
": 4,
|
| 97 |
+
"γ": 5,
|
| 98 |
+
"γ": 6,
|
| 99 |
+
"γ": 7,
|
| 100 |
+
"γ": 8,
|
| 101 |
+
"γ": 9,
|
| 102 |
+
"γ": 10,
|
| 103 |
+
"γ": 11,
|
| 104 |
+
"γ": 12,
|
| 105 |
+
"γ": 13,
|
| 106 |
+
"γ": 14,
|
| 107 |
+
"γ": 15,
|
| 108 |
+
"γ": 16,
|
| 109 |
+
"γ": 17,
|
| 110 |
+
"γ": 18,
|
| 111 |
+
"γ": 19,
|
| 112 |
+
"γ": 20,
|
| 113 |
+
"γ": 21,
|
| 114 |
+
"γ": 22,
|
| 115 |
+
"γ": 23,
|
| 116 |
+
"γ": 24,
|
| 117 |
+
"γ": 25,
|
| 118 |
+
"γ": 26,
|
| 119 |
+
"γ": 27,
|
| 120 |
+
"γ": 28,
|
| 121 |
+
"γ": 29,
|
| 122 |
+
"γ": 30,
|
| 123 |
+
"γ ": 31,
|
| 124 |
+
"γ‘": 32,
|
| 125 |
+
"γ’": 33,
|
| 126 |
+
"γ£": 34,
|
| 127 |
+
"γ€": 35,
|
| 128 |
+
"γ₯": 36,
|
| 129 |
+
"γ¦": 37,
|
| 130 |
+
"γ§": 38,
|
| 131 |
+
"γ¨": 39,
|
| 132 |
+
"γ©": 40,
|
| 133 |
+
"AA": 41,
|
| 134 |
+
"AE": 42,
|
| 135 |
+
"AH": 43,
|
| 136 |
+
"AO": 44,
|
| 137 |
+
"AW": 45,
|
| 138 |
+
"AY": 46,
|
| 139 |
+
"B": 47,
|
| 140 |
+
"CH": 48,
|
| 141 |
+
"D": 49,
|
| 142 |
+
"DH": 50,
|
| 143 |
+
"EH": 51,
|
| 144 |
+
"ER": 52,
|
| 145 |
+
"EY": 53,
|
| 146 |
+
"F": 54,
|
| 147 |
+
"G": 55,
|
| 148 |
+
"HH": 56,
|
| 149 |
+
"IH": 57,
|
| 150 |
+
"IY": 58,
|
| 151 |
+
"JH": 59,
|
| 152 |
+
"K": 60,
|
| 153 |
+
"L": 61,
|
| 154 |
+
"M": 62,
|
| 155 |
+
"N": 63,
|
| 156 |
+
"NG": 64,
|
| 157 |
+
"OW": 65,
|
| 158 |
+
"OY": 66,
|
| 159 |
+
"P": 67,
|
| 160 |
+
"R": 68,
|
| 161 |
+
"S": 69,
|
| 162 |
+
"SH": 70,
|
| 163 |
+
"T": 71,
|
| 164 |
+
"TH": 72,
|
| 165 |
+
"UH": 73,
|
| 166 |
+
"UW": 74,
|
| 167 |
+
"V": 75,
|
| 168 |
+
"W": 76,
|
| 169 |
+
"Y": 77,
|
| 170 |
+
"Z": 78,
|
| 171 |
+
"ZH": 79,
|
| 172 |
+
",": 80,
|
| 173 |
+
".": 81,
|
| 174 |
+
"?": 82,
|
| 175 |
+
"!": 83,
|
| 176 |
+
"β¦": 84,
|
| 177 |
+
"-": 85,
|
| 178 |
+
"'": 86
|
| 179 |
+
},
|
| 180 |
+
"lang": {
|
| 181 |
+
"ZH": 0,
|
| 182 |
+
"EN": 1
|
| 183 |
+
},
|
| 184 |
+
"n_symbols": 87,
|
| 185 |
+
"max_tone": 5
|
| 186 |
+
}
|
scripts/synth_from_text.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""End-to-end synthesis from text via the exported 8k ONNX pipeline:
|
| 3 |
+
text -> bopomofo+arpabet frontend -> ids -> encoder.onnx -> numpy host_regulate
|
| 4 |
+
-> decoder.onnx -> vocoder.onnx -> 8kHz wav. Run in moss-train-venv (g2pw+ort).
|
| 5 |
+
Used for M1 eval (synthesize zh-TW/en/code-mix test sentences). X-ASR scoring is a
|
| 6 |
+
separate step in moss-nano-venv (xasr_offline.py) on the produced wavs."""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import argparse, json, sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import numpy as np, soundfile as sf, onnxruntime as ort
|
| 11 |
+
|
| 12 |
+
ZT = "/home/luigi/jetson-tts/mossnano/zhtw8k"
|
| 13 |
+
sys.path.insert(0, ZT)
|
| 14 |
+
import frontend_bopomofo as F # g2pw bopomofo + g2p_en arpabet -> ids
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def host_regulate(cond, dur, pitch, abs_bins, max_frames):
|
| 18 |
+
c = cond[0]; d = dur[0].astype(np.int64); d[d < 0] = 0
|
| 19 |
+
T, H = c.shape
|
| 20 |
+
frames = np.repeat(c, d, axis=0); Fn = frames.shape[0]
|
| 21 |
+
tok = np.repeat(np.arange(T), d); starts = np.cumsum(d) - d
|
| 22 |
+
within = np.arange(Fn) - starts[tok]; dpf = d[tok].astype(np.float32)
|
| 23 |
+
rel = (within / np.maximum(dpf - 1, 1)).astype(np.float32)
|
| 24 |
+
tc = max(1, int((d > 0).sum())); token_pos = (tok / max(1, tc - 1)).astype(np.float32)
|
| 25 |
+
ld = (np.log1p(dpf) / 6.0).astype(np.float32); center = 1.0 - np.abs(rel * 2 - 1)
|
| 26 |
+
fm = np.stack([rel, 1 - rel, center, np.sin(rel*np.pi), np.cos(rel*np.pi), token_pos, ld, dpf/40.0], -1).astype(np.float32)
|
| 27 |
+
prev = np.concatenate([c[:1], c[:-1]], 0); nxt = np.concatenate([c[1:], c[-1:]], 0)
|
| 28 |
+
lc = np.repeat(np.concatenate([prev, c, nxt], -1), d, axis=0).astype(np.float32)
|
| 29 |
+
pos = np.arange(Fn); ap = np.minimum(pos*abs_bins//max(1, max_frames), abs_bins-1).astype(np.int64)
|
| 30 |
+
pf = np.repeat(pitch[0], d, axis=0).astype(np.float32)
|
| 31 |
+
return {"frames": frames[None].astype(np.float32), "frame_meta": fm[None], "local_ctx_raw": lc[None],
|
| 32 |
+
"abs_pos": ap[None], "pitch_frame": pf[None], "frame_mask": np.ones((1, Fn), bool)}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def main():
|
| 36 |
+
ap = argparse.ArgumentParser()
|
| 37 |
+
ap.add_argument("--onnx-dir", required=True)
|
| 38 |
+
ap.add_argument("--out-dir", required=True)
|
| 39 |
+
ap.add_argument("--texts", required=True, help="jsonl with {id,text}")
|
| 40 |
+
args = ap.parse_args()
|
| 41 |
+
meta = json.load(open(f"{args.onnx_dir}/meta.json"))
|
| 42 |
+
so = ort.SessionOptions(); so.intra_op_num_threads = 4
|
| 43 |
+
sA = ort.InferenceSession(f"{args.onnx_dir}/acoustic_encoder.onnx", so, providers=["CPUExecutionProvider"])
|
| 44 |
+
sB = ort.InferenceSession(f"{args.onnx_dir}/acoustic_decoder.onnx", so, providers=["CPUExecutionProvider"])
|
| 45 |
+
sV = ort.InferenceSession(f"{args.onnx_dir}/vocoder.onnx", so, providers=["CPUExecutionProvider"])
|
| 46 |
+
Path(args.out_dir).mkdir(parents=True, exist_ok=True)
|
| 47 |
+
sr = meta["sample_rate"]; bn = ["frames","frame_meta","local_ctx_raw","abs_pos","pitch_frame","frame_mask"]
|
| 48 |
+
rows = [json.loads(l) for l in open(args.texts) if l.strip()]
|
| 49 |
+
out_manifest = open(f"{args.out_dir}/synth.jsonl", "w")
|
| 50 |
+
for r in rows:
|
| 51 |
+
o = F.text_to_ids(r["text"])
|
| 52 |
+
phone = np.array([o["phone_ids"]], np.int64); tone = np.array([o["tone_ids"]], np.int64); lang = np.array([o["lang_ids"]], np.int64)
|
| 53 |
+
spk = np.zeros(1, np.int64)
|
| 54 |
+
cond, dur, pitch = sA.run(None, {"phone": phone, "tone": tone, "lang": lang, "speaker": spk})
|
| 55 |
+
reg = host_regulate(cond, dur, pitch, meta["abs_frame_bins"], meta["max_frames"])
|
| 56 |
+
feeds = {n: (reg[n].astype(np.float32) if reg[n].dtype != bool else reg[n]) for n in bn}
|
| 57 |
+
feeds["abs_pos"] = reg["abs_pos"].astype(np.int64)
|
| 58 |
+
mel = sB.run(None, feeds)[0]
|
| 59 |
+
wav = sV.run(None, {"mel": mel.astype(np.float32)})[0].reshape(-1)
|
| 60 |
+
wp = f"{args.out_dir}/{r['id']}.wav"; sf.write(wp, wav, sr)
|
| 61 |
+
out_manifest.write(json.dumps({"id": r["id"], "text": r["text"], "wav": wp, "dur": round(len(wav)/sr, 2)}, ensure_ascii=False) + "\n")
|
| 62 |
+
print(f" {r['id']}: {len(wav)/sr:.1f}s -> {wp}")
|
| 63 |
+
out_manifest.close()
|
| 64 |
+
print(f"DONE synth -> {args.out_dir}/synth.jsonl")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
main()
|
scripts/xasr_offline.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Offline X-ASR (zh-en zipformer transducer, 2026-06-03) scorer β accurate code-mix
|
| 2 |
+
assessment. Reuses the model's own raw-ort decode (int64-patched) + OpenCC T2S norm.
|
| 3 |
+
Run in moss-nano-venv (onnxruntime 1.27 + kaldi_native_fbank + opencc + soundfile/librosa)."""
|
| 4 |
+
import os, re, sys
|
| 5 |
+
MDIR="/home/luigi/jetson-tts/models/xasr-offline/sherpa-onnx-x-asr-zipformer-transducer-zh-en-2026-06-03"
|
| 6 |
+
_cwd=os.getcwd(); os.chdir(MDIR); sys.path.insert(0, MDIR)
|
| 7 |
+
import importlib.util
|
| 8 |
+
_spec=importlib.util.spec_from_file_location("xasr_test", f"{MDIR}/test_onnx.py")
|
| 9 |
+
T=importlib.util.module_from_spec(_spec); _spec.loader.exec_module(T)
|
| 10 |
+
os.chdir(_cwd)
|
| 11 |
+
import opencc, cn2an
|
| 12 |
+
_ZD={"0":"ιΆ","1":"δΈ","2":"δΊ","3":"δΈ","4":"ε","5":"δΊ","6":"ε
","7":"δΈ","8":"ε
«","9":"δΉ"}
|
| 13 |
+
def _numnorm(t):
|
| 14 |
+
def r(m):
|
| 15 |
+
d=m.group(0)
|
| 16 |
+
if len(d)>=5: return "".join(_ZD[c] for c in d)
|
| 17 |
+
try: return cn2an.an2cn(d,"low")
|
| 18 |
+
except: return "".join(_ZD[c] for c in d)
|
| 19 |
+
return re.sub(r"\d+",r,t)
|
| 20 |
+
_t2s=opencc.OpenCC('t2s'); _model=None; _id2tok=None
|
| 21 |
+
def _ensure():
|
| 22 |
+
global _model,_id2tok
|
| 23 |
+
if _model is None:
|
| 24 |
+
cwd=os.getcwd(); os.chdir(MDIR)
|
| 25 |
+
_id2tok=T.load_tokens("./tokens.txt"); _model=T.load_model(use_int8=False)
|
| 26 |
+
os.chdir(cwd)
|
| 27 |
+
return _model,_id2tok
|
| 28 |
+
def asr(wav):
|
| 29 |
+
import soundfile as sf, librosa, numpy as np
|
| 30 |
+
m,id2tok=_ensure()
|
| 31 |
+
samples,sr=sf.read(wav,dtype="float32")
|
| 32 |
+
if samples.ndim>1: samples=samples.mean(1)
|
| 33 |
+
if sr!=16000: samples=librosa.resample(samples,orig_sr=sr,target_sr=16000,res_type="soxr_hq")
|
| 34 |
+
if len(samples)<64000: samples=np.concatenate([samples,np.zeros(64000-len(samples),np.float32)]) # >=4s pad: short clips break encoder convs
|
| 35 |
+
feats=T.compute_feat(samples=samples.astype(np.float32), sample_rate=16000)
|
| 36 |
+
blank=0; hyp=[blank]*m.context_size
|
| 37 |
+
dout=m.run_decoder(hyp); enc=m.run_encoder(feats[None])
|
| 38 |
+
for k in range(enc.shape[1]):
|
| 39 |
+
jo=m.run_joiner(enc[0,k:k+1], dout); tid=int(jo.argmax())
|
| 40 |
+
if tid!=blank:
|
| 41 |
+
hyp.append(tid); dout=m.run_decoder(hyp[-m.context_size:])
|
| 42 |
+
toks=[id2tok[i] for i in hyp[m.context_size:]]
|
| 43 |
+
return "".join(toks).replace("β"," ").strip()
|
| 44 |
+
def han(s): return "".join(c for c in s if "δΈ"<=c<="ιΏΏ")
|
| 45 |
+
def _lev(r,h):
|
| 46 |
+
if not r: return 0.0
|
| 47 |
+
d=list(range(len(h)+1))
|
| 48 |
+
for i in range(1,len(r)+1):
|
| 49 |
+
p=d[0]; d[0]=i
|
| 50 |
+
for j in range(1,len(h)+1):
|
| 51 |
+
c=d[j]; d[j]=min(d[j]+1,d[j-1]+1,p+(r[i-1]!=h[j-1])); p=c
|
| 52 |
+
return d[len(h)]/len(r)
|
| 53 |
+
def score(ref,hyp):
|
| 54 |
+
is_zh=bool(re.search(r"[δΈ-ιΏΏ]",ref))
|
| 55 |
+
if is_zh: return _lev(han(_t2s.convert(_numnorm(ref))),han(_t2s.convert(hyp)))
|
| 56 |
+
return _lev(re.findall(r"[a-z']+",ref.lower()),re.findall(r"[a-z']+",hyp.lower()))
|
weights/acoustic_decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ea97d8caf52af44a6372aa6cb445de6b70690561d02d2cf8514c019fbad91bc
|
| 3 |
+
size 8147039
|
weights/acoustic_encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:605048511cd3c8ab429768a8b11f7e2d45705379c9e6ff91200e88ab8664c18f
|
| 3 |
+
size 5782030
|
weights/acoustic_zh_v2_60k.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:559970e6a52d5ea9b7aaecf592835f8c589fe8b706223125a62a5e4c618ed670
|
| 3 |
+
size 41829011
|
weights/meta.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sample_rate": 8000,
|
| 3 |
+
"abs_frame_bins": 512,
|
| 4 |
+
"max_frames": 1000,
|
| 5 |
+
"hop_size": 128,
|
| 6 |
+
"n_mels": 80,
|
| 7 |
+
"use_frame_pitch": true
|
| 8 |
+
}
|
weights/symbol_table.json
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"symbols": [
|
| 3 |
+
"_blank",
|
| 4 |
+
"_pad",
|
| 5 |
+
"UNK",
|
| 6 |
+
"SP",
|
| 7 |
+
"γ
",
|
| 8 |
+
"γ",
|
| 9 |
+
"γ",
|
| 10 |
+
"γ",
|
| 11 |
+
"γ",
|
| 12 |
+
"γ",
|
| 13 |
+
"γ",
|
| 14 |
+
"γ",
|
| 15 |
+
"γ",
|
| 16 |
+
"γ",
|
| 17 |
+
"γ",
|
| 18 |
+
"γ",
|
| 19 |
+
"γ",
|
| 20 |
+
"γ",
|
| 21 |
+
"γ",
|
| 22 |
+
"γ",
|
| 23 |
+
"γ",
|
| 24 |
+
"γ",
|
| 25 |
+
"γ",
|
| 26 |
+
"γ",
|
| 27 |
+
"γ",
|
| 28 |
+
"γ",
|
| 29 |
+
"γ",
|
| 30 |
+
"γ",
|
| 31 |
+
"γ",
|
| 32 |
+
"γ",
|
| 33 |
+
"γ",
|
| 34 |
+
"γ ",
|
| 35 |
+
"γ‘",
|
| 36 |
+
"γ’",
|
| 37 |
+
"γ£",
|
| 38 |
+
"γ€",
|
| 39 |
+
"γ₯",
|
| 40 |
+
"γ¦",
|
| 41 |
+
"γ§",
|
| 42 |
+
"γ¨",
|
| 43 |
+
"γ©",
|
| 44 |
+
"AA",
|
| 45 |
+
"AE",
|
| 46 |
+
"AH",
|
| 47 |
+
"AO",
|
| 48 |
+
"AW",
|
| 49 |
+
"AY",
|
| 50 |
+
"B",
|
| 51 |
+
"CH",
|
| 52 |
+
"D",
|
| 53 |
+
"DH",
|
| 54 |
+
"EH",
|
| 55 |
+
"ER",
|
| 56 |
+
"EY",
|
| 57 |
+
"F",
|
| 58 |
+
"G",
|
| 59 |
+
"HH",
|
| 60 |
+
"IH",
|
| 61 |
+
"IY",
|
| 62 |
+
"JH",
|
| 63 |
+
"K",
|
| 64 |
+
"L",
|
| 65 |
+
"M",
|
| 66 |
+
"N",
|
| 67 |
+
"NG",
|
| 68 |
+
"OW",
|
| 69 |
+
"OY",
|
| 70 |
+
"P",
|
| 71 |
+
"R",
|
| 72 |
+
"S",
|
| 73 |
+
"SH",
|
| 74 |
+
"T",
|
| 75 |
+
"TH",
|
| 76 |
+
"UH",
|
| 77 |
+
"UW",
|
| 78 |
+
"V",
|
| 79 |
+
"W",
|
| 80 |
+
"Y",
|
| 81 |
+
"Z",
|
| 82 |
+
"ZH",
|
| 83 |
+
",",
|
| 84 |
+
".",
|
| 85 |
+
"?",
|
| 86 |
+
"!",
|
| 87 |
+
"β¦",
|
| 88 |
+
"-",
|
| 89 |
+
"'"
|
| 90 |
+
],
|
| 91 |
+
"sym2id": {
|
| 92 |
+
"_blank": 0,
|
| 93 |
+
"_pad": 1,
|
| 94 |
+
"UNK": 2,
|
| 95 |
+
"SP": 3,
|
| 96 |
+
"γ
": 4,
|
| 97 |
+
"γ": 5,
|
| 98 |
+
"γ": 6,
|
| 99 |
+
"γ": 7,
|
| 100 |
+
"γ": 8,
|
| 101 |
+
"γ": 9,
|
| 102 |
+
"γ": 10,
|
| 103 |
+
"γ": 11,
|
| 104 |
+
"γ": 12,
|
| 105 |
+
"γ": 13,
|
| 106 |
+
"γ": 14,
|
| 107 |
+
"γ": 15,
|
| 108 |
+
"γ": 16,
|
| 109 |
+
"γ": 17,
|
| 110 |
+
"γ": 18,
|
| 111 |
+
"γ": 19,
|
| 112 |
+
"γ": 20,
|
| 113 |
+
"γ": 21,
|
| 114 |
+
"γ": 22,
|
| 115 |
+
"γ": 23,
|
| 116 |
+
"γ": 24,
|
| 117 |
+
"γ": 25,
|
| 118 |
+
"γ": 26,
|
| 119 |
+
"γ": 27,
|
| 120 |
+
"γ": 28,
|
| 121 |
+
"γ": 29,
|
| 122 |
+
"γ": 30,
|
| 123 |
+
"γ ": 31,
|
| 124 |
+
"γ‘": 32,
|
| 125 |
+
"γ’": 33,
|
| 126 |
+
"γ£": 34,
|
| 127 |
+
"γ€": 35,
|
| 128 |
+
"γ₯": 36,
|
| 129 |
+
"γ¦": 37,
|
| 130 |
+
"γ§": 38,
|
| 131 |
+
"γ¨": 39,
|
| 132 |
+
"γ©": 40,
|
| 133 |
+
"AA": 41,
|
| 134 |
+
"AE": 42,
|
| 135 |
+
"AH": 43,
|
| 136 |
+
"AO": 44,
|
| 137 |
+
"AW": 45,
|
| 138 |
+
"AY": 46,
|
| 139 |
+
"B": 47,
|
| 140 |
+
"CH": 48,
|
| 141 |
+
"D": 49,
|
| 142 |
+
"DH": 50,
|
| 143 |
+
"EH": 51,
|
| 144 |
+
"ER": 52,
|
| 145 |
+
"EY": 53,
|
| 146 |
+
"F": 54,
|
| 147 |
+
"G": 55,
|
| 148 |
+
"HH": 56,
|
| 149 |
+
"IH": 57,
|
| 150 |
+
"IY": 58,
|
| 151 |
+
"JH": 59,
|
| 152 |
+
"K": 60,
|
| 153 |
+
"L": 61,
|
| 154 |
+
"M": 62,
|
| 155 |
+
"N": 63,
|
| 156 |
+
"NG": 64,
|
| 157 |
+
"OW": 65,
|
| 158 |
+
"OY": 66,
|
| 159 |
+
"P": 67,
|
| 160 |
+
"R": 68,
|
| 161 |
+
"S": 69,
|
| 162 |
+
"SH": 70,
|
| 163 |
+
"T": 71,
|
| 164 |
+
"TH": 72,
|
| 165 |
+
"UH": 73,
|
| 166 |
+
"UW": 74,
|
| 167 |
+
"V": 75,
|
| 168 |
+
"W": 76,
|
| 169 |
+
"Y": 77,
|
| 170 |
+
"Z": 78,
|
| 171 |
+
"ZH": 79,
|
| 172 |
+
",": 80,
|
| 173 |
+
".": 81,
|
| 174 |
+
"?": 82,
|
| 175 |
+
"!": 83,
|
| 176 |
+
"β¦": 84,
|
| 177 |
+
"-": 85,
|
| 178 |
+
"'": 86
|
| 179 |
+
},
|
| 180 |
+
"lang": {
|
| 181 |
+
"ZH": 0,
|
| 182 |
+
"EN": 1
|
| 183 |
+
},
|
| 184 |
+
"n_symbols": 87,
|
| 185 |
+
"max_tone": 5
|
| 186 |
+
}
|
weights/vocoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa20f208f9d006f07ad00965809aea72f461e5280f8592412f6d371095a2636f
|
| 3 |
+
size 4705722
|