Update README

This commit is contained in:
Michael Hansen 2025-08-29 15:16:20 -05:00
parent 4057c1a620
commit 4d7e4b390c
3 changed files with 155 additions and 123 deletions

101
README.md
View file

@ -1,14 +1,8 @@
# Piper Sample Generator
Generates samples using [Piper](https://github.com/rhasspy/piper/) for training a wake word system like [openWakeWord](https://github.com/dscripka/openWakeWord).
Available models:
* [English](https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt)
* [French](https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/fr_FR-mls-medium.pt)
* [German](https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/de_DE-mls-medium.pt)
* [Dutch](https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/nl_NL-mls-medium.pt)
Generate spoken audio samples using [Piper][piper] for training a wake word system like [openWakeWord][] or [microWakeWord][].
Supports normal [Piper voices][piper voices] or a special [generator][] that can mix speaker embeddings (English only).
## Install
@ -21,23 +15,45 @@ cd piper-sample-generator/
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install -e .
```
Download the LibriTTS-R generator (exported from [checkpoint](https://huggingface.co/datasets/rhasspy/piper-checkpoints/tree/main/en/en_US/libritts_r/medium)):
## Piper Voices
Download one or more [Piper voices][piper voices] (both the `.onnx` and `.onnx.json` files for each voice). [Audio samples][piper samples] are available.
As an example, we'll download the U.S. English "lessac" voice in medium quality:
``` sh
mkdir -p voices
wget -O voices/en_US-lessac-medium.onnx 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx?download=true'
wget -O voices/en_US-lessac-medium.onnx.json 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json?download=true'
```
Generate a small set of samples with the CLI:
``` sh
python3 generate_samples.py 'okay piper.' --model voices/en_US-lessac-medium.onnx --max-samples 10 --output-dir okay_piper/
```
Check the `okay_piper/` directory for 10 WAV files (named `0.wav` to `9.wav`).
You can add multiple `--model <voice>` arguments to cycle between different voices when generating samples.
See `--help` for more options, including `--length-scales` (speaking speeds).
## Generator
Download the LibriTTS-R generator (exported from [checkpoint][]):
``` sh
wget -O models/en-us-libritts-high.pt 'https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt'
```
See links above for models for other languages.
## Run
Generate a small set of samples with the CLI:
``` sh
python3 generate_samples.py 'okay, piper.' --max-samples 10 --output-dir okay_piper/
python3 generate_samples.py 'okay piper.' --model models/en-us-libritts-high.pt --max-samples 10 --output-dir okay_piper/
```
Check the `okay_piper/` directory for 10 WAV files (named `0.wav` to `9.wav`).
@ -45,63 +61,38 @@ Check the `okay_piper/` directory for 10 WAV files (named `0.wav` to `9.wav`).
Generation can be much faster and more efficient if you have a GPU available and PyTorch is configured to use it. In this case, increase the batch size:
``` sh
python3 generate_samples.py 'okay, piper.' --max-samples 100 --batch-size 10 --output-dir okay_piper/
python3 generate_samples.py 'okay piper.' --model models/en-us-libritts-high.pt --max-samples 100 --batch-size 10 --output-dir okay_piper/
```
On an NVidia 2080 Ti with 11GB, a batch size of 100 was possible (generating approximately 100 samples per second).
Setting `--max-speakers` to a value less than 904 (the number of speakers LibriTTS) is recommended. Because very few samples of later speakers were in the original dataset, using them can cause audio artifacts.
See `--help` for more options, including adjust the `--length-scales` (speaking speeds) and `--slerp-weights` (speaker blending) which are cycled per batch.
Alternatively, you can import the generate function into another Python script:
```python
from generate_samples import generate_samples # make sure to add this to your Python path as needed
generate_samples(text = ["okay, piper"], max_samples = 100, output_dir = output_dir, batch_size=10)
```
There are some additional arguments available when importing the function directly, see the docstring of `generate_sample` for more information.
See `--help` for more options, including the `--length-scales` (speaking speeds) and `--slerp-weights` (speaker blending) which are cycled per batch.
### Augmentation
Once you have samples generating, you can augment them using [audiomentation](https://iver56.github.io/audiomentations/):
Once you have samples generated, you can augment them using [audiomentation](https://iver56.github.io/audiomentations/):
``` sh
python3 augment.py --sample-rate 16000 okay_piper/ okay_piper_augmented/
python3 augment.py --sample-rate 22050 okay_piper/ okay_piper_augmented/
```
This will do several things to each sample:
1. Randomly decrease the volume
* The original samples are normalized, so different volume levels are needed
2. Randomly [apply an impulse response](https://iver56.github.io/audiomentations/waveform_transforms/apply_impulse_response/) using the files in `impulses/`
2. Randomly apply an [impulse response][] using the files in `impulses/`
* Change the acoustics of the sample to sound like the speaker was in a room with echo or using a poor quality microphone
3. Resample to 16Khz for training (e.g., [openWakeWord](https://github.com/dscripka/openWakeWord))
3. Resample to 16Khz for training (e.g., [openWakeWord][])
## Short Phrases
Models that were trained on audio books tend to perform poorly when speaking short phrases or single words.
The French, German, and Dutch models trained from the [MLS](http://openslr.org/94/) have this problem.
The problem can be mitigated by repeating the phrase over and over, and then clipping out a single sample.
To do this automatically, follow these steps:
1. Ensure your short phrase ends with a comma (`<phrase>,`)
2. Lower the noise settings with `--noise-scales 0.333` and `--noise-scale-ws 0.333`
3. Use `--min-phoneme-count 300` (the value 300 was determined empirically and may be less for some models)
For example:
``` sh
python3 generate_samples.py \
'framboise,' \
--model models/fr_FR-mls-medium.pt \
--noise-scales 0.333 \
--noise-scale-ws 0.333 \
--min-phoneme-count 300
--max-samples 1 \
--output-dir .
```
<!-- Links -->
[piper]: https://github.com/OHF-Voice/piper1-gpl/
[openWakeWord]: https://github.com/dscripka/openWakeWord
[microWakeWord]: https://github.com/kahrendt/microWakeWord/
[piper voices]: https://huggingface.co/rhasspy/piper-voices
[generator]: https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt
[piper samples]: https://rhasspy.github.io/piper-samples/
[checkpoint]: https://huggingface.co/datasets/rhasspy/piper-checkpoints/tree/main/en/en_US/libritts_r/medium
[impulse response]: https://iver56.github.io/audiomentations/waveform_transforms/apply_impulse_response/

View file

@ -22,7 +22,7 @@ def main() -> None:
augment = Compose(
transforms=[
Gain(min_gain_in_db=-12, max_gain_in_db=0),
Gain(min_gain_db=-12, max_gain_db=0),
ApplyImpulseResponse(impulses),
]
)

View file

@ -26,9 +26,9 @@ logging.basicConfig(level=logging.DEBUG)
def generate_samples(
text: Union[List[str], str],
output_dir: Union[str, Path],
model: Union[str, Path],
max_samples: Optional[int] = None,
file_names: Optional[Iterable[str]] = None,
model: Union[str, Path] = _DIR / "models" / "en_US-libritts_r-medium.pt",
batch_size: int = 1,
slerp_weights: Tuple[float, ...] = (0.5,),
length_scales: Tuple[float, ...] = (0.75, 1, 1.25),
@ -45,10 +45,10 @@ def generate_samples(
text (List[str]): The text to convert into speech. Can be either a
a list of strings, or a path to a file with text on each line.
output_dir (str): The location to save the generated clips.
model (str): The path to the TTS generator model (.pt).
max_samples (int): The maximum number of samples to generate.
file_names (List[str]): The names to use when saving the files. Must be the same length
as the `text` argument, if a list.
model (str): The path to the TTS generator model (.pt).
batch_size (int): The batch size to use when generated the clips
slerp_weights (List[float]): The weights to use when mixing speakers via SLERP.
length_scales (List[float]): Controls the average duration/speed of the generated speech.
@ -100,17 +100,6 @@ def generate_samples(
)
)
# Define resampler to get to 16khz (https://pytorch.org/audio/stable/tutorials/audio_resampling_tutorial.html#kaiser-best)
resample_rate = 16000
resampler = torchaudio.transforms.Resample(
sample_rate,
resample_rate,
lowpass_filter_width=64,
rolloff=0.9475937167399596,
# resampling_method="sinc_interp_kaiser",
beta=14.769656459379492,
)
speakers_iter = it.cycle(it.product(range(num_speakers), range(num_speakers)))
speakers_batch = list(it.islice(speakers_iter, 0, batch_size))
if isinstance(text, str) and os.path.exists(text):
@ -157,22 +146,23 @@ def generate_samples(
return padded_lists
phoneme_ids_by_batch = right_pad_lists(phoneme_ids_by_batch)
audio = generate_audio(
torch_model,
speaker_1,
speaker_2,
phoneme_ids_by_batch,
slerp_weight,
noise_scale,
noise_scale_w,
length_scale,
max_len,
audio = (
generate_audio(
torch_model,
speaker_1,
speaker_2,
phoneme_ids_by_batch,
slerp_weight,
noise_scale,
noise_scale_w,
length_scale,
max_len,
)
.cpu()
.numpy()
)
# Resample audio
audio_np = resampler(audio.cpu()).numpy()
audio_int16 = audio_float_to_int16(audio_np)
audio_int16 = audio_float_to_int16(audio)
for audio_idx in range(audio_int16.shape[0]):
audio_data = np.trim_zeros(audio_int16[audio_idx].flatten())
@ -183,7 +173,7 @@ def generate_samples(
wav_file: wave.Wave_write = wave.open(str(wav_path), "wb")
with wav_file:
wav_file.setframerate(resample_rate)
wav_file.setframerate(sample_rate)
wav_file.setsampwidth(2)
wav_file.setnchannels(1)
wav_file.writeframes(audio_data)
@ -209,7 +199,7 @@ def generate_samples(
def generate_samples_onnx(
text: Union[List[str], str],
output_dir: Union[str, Path],
model: Union[str, Path],
model: Union[str, Path, List[Union[str, Path]]],
max_samples: Optional[int] = None,
file_names: Optional[Iterable[str]] = None,
length_scales: Tuple[float, ...] = (0.75, 1, 1.25),
@ -241,21 +231,20 @@ def generate_samples_onnx(
if max_samples is None:
max_samples = len(text)
if not isinstance(model, list):
model = [model]
_LOGGER.debug("Loading %s", model)
voice = PiperVoice.load(model, use_cuda=torch.cuda.is_available())
_LOGGER.info("Successfully loaded the model")
voices = [PiperVoice.load(m, use_cuda=torch.cuda.is_available()) for m in model]
_LOGGER.info("Successfully loaded model(s)")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
num_speakers = voice.config.num_speakers
if max_speakers is not None:
num_speakers = min(num_speakers, max_speakers)
sample_idx = 0
settings_iter = it.cycle(
it.product(
list(range(num_speakers)),
voices,
length_scales,
noise_scales,
noise_scale_ws,
@ -278,27 +267,32 @@ def generate_samples_onnx(
if file_names:
file_names = it.cycle(file_names)
for speaker_id, length_scale, noise_scale, noise_w_scale in settings_iter:
if isinstance(file_names, it.cycle):
wav_path = output_dir / next(file_names)
else:
wav_path = output_dir / f"{sample_idx}.wav"
for voice, length_scale, noise_scale, noise_w_scale in settings_iter:
num_speakers = voice.config.num_speakers
if max_speakers is not None:
num_speakers = min(num_speakers, max_speakers)
wav_file: wave.Wave_write = wave.open(str(wav_path), "wb")
voice.synthesize_wav(
next(texts),
wav_file=wav_file,
syn_config=SynthesisConfig(
speaker_id=speaker_id,
length_scale=length_scale,
noise_scale=noise_scale,
noise_w_scale=noise_w_scale,
),
)
for speaker_id in range(num_speakers):
if isinstance(file_names, it.cycle):
wav_path = output_dir / next(file_names)
else:
wav_path = output_dir / f"{sample_idx}.wav"
sample_idx += 1
if sample_idx >= max_samples:
break
wav_file: wave.Wave_write = wave.open(str(wav_path), "wb")
voice.synthesize_wav(
next(texts),
wav_file=wav_file,
syn_config=SynthesisConfig(
speaker_id=speaker_id,
length_scale=length_scale,
noise_scale=noise_scale,
noise_w_scale=noise_w_scale,
),
)
sample_idx += 1
if sample_idx >= max_samples:
return
_LOGGER.info("Done")
@ -459,45 +453,92 @@ def audio_float_to_int16(
# -----------------------------------------------------------------------------
def main() -> None:
def main() -> int:
"""Main entry point."""
# Get command line arguments
parser = argparse.ArgumentParser()
parser.add_argument("text")
parser.add_argument("--max-samples", required=True, type=int)
parser.add_argument(
"--model", default=_DIR / "models" / "en_US-libritts_r-medium.pt"
"--max-samples",
required=True,
type=int,
help="Maximum number of samples to generate",
)
parser.add_argument("--batch-size", type=int, default=1)
parser.add_argument("--slerp-weights", nargs="+", type=float, default=[0.5])
parser.add_argument(
"--length-scales", nargs="+", type=float, default=[1.0, 0.75, 1.25, 1.4]
"--model",
required=True,
action="append",
help="Path to PyTorch generator (.pt) or Piper voice model (.onnx)",
)
parser.add_argument(
"--batch-size", type=int, default=1, help="CUDA batch size (generator only)"
)
parser.add_argument(
"--slerp-weights",
nargs="+",
type=float,
default=[0.5],
help="Speaker blending weights (generator only)",
)
parser.add_argument(
"--length-scales",
nargs="+",
type=float,
default=[1.0, 0.75, 1.25, 1.4],
help="Audio length scales (< 1 is faster, > 1 is slower)",
)
parser.add_argument(
"--noise-scales",
nargs="+",
type=float,
default=[0.667, 0.75, 0.85, 0.9, 1.0, 1.4],
help="Noise amounts added to audio (most voices use 0.667)",
)
parser.add_argument(
"--noise-scale-ws",
nargs="+",
type=float,
default=[0.8],
help="Phoneme width variation (most voices use 0.8)",
)
parser.add_argument(
"--output-dir",
default="output",
help="Directory to output WAV files (default: ./output)",
)
parser.add_argument("--noise-scale-ws", nargs="+", type=float, default=[0.8])
parser.add_argument("--output-dir", default="output")
parser.add_argument(
"--max-speakers",
type=int,
help="Maximum number of speakers to use (default: all)",
help="Maximum number of speakers to use (default: no limit)",
)
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args().__dict__
# Generate speech
model_path = Path(args["model"])
if model_path.suffix == ".onnx":
model_paths = [Path(m) for m in args["model"]]
assert model_paths
if any(mp for mp in model_paths[1:] if mp.suffix != model_paths[0].suffix):
_LOGGER.error("All models must have the same suffix (.pt or .onnx)")
return 1
if model_paths[0].suffix == ".onnx":
# Use Piper voice (.onnx)
generate_samples_onnx(**args)
else:
elif model_paths[0].suffix == ".pt":
# Use PyTorch generator (.pt)
if len(model_paths) > 1:
_LOGGER.error("Only one generator (.pt) is supported")
return 1
args["model"] = args["model"][0]
generate_samples(**args)
else:
_LOGGER.error("Models must have .pt or .onnx suffix")
return 1
return 0
if __name__ == "__main__":