// Copyright 2025 Tencent // SPDX-License-Identifier: BSD-3-Clause // convert piper checkpoints to ncnn models // 1. checkout https://github.com/OHF-Voice/piper1-gpl (113931937cf235fc881afd1ca4be209bc6919bc7) // 2. apply patch piper1-gpl.patch from https://github.com/nihui/ncnn-android-piper // 3. setup piper with // python3 -m venv .venv // source .venv/bin/activate // python3 -m pip install -e .[train] // 4. download piper checkpoint file (*.ckpt) from https://huggingface.co/datasets/rhasspy/piper-checkpoints // 5. install pnnx via pip install -U pnnx // 6. obtain export_ncnn.py script from https://github.com/nihui/ncnn-android-piper // python export_ncnn.py en.ckpt // convert word list to simple phonemizer dict // 1. prepare word list from https://github.com/Alexir/CMUdict // 2. for each word, get phonemes via command "./espeak-ng -q -v en-us --ipa word" // 3. obtain config.json file from https://huggingface.co/datasets/rhasspy/piper-checkpoints // 4. replace phonemes with ids according to phoneme_id_map in config.json // 5. write dict binary // word1 \0x00 ids1 \0xff word2 \0x00 ids2 \0xff ..... #include "layer.h" #include "mat.h" #include "net.h" #include #include #include #include class relative_embeddings_k_module : public ncnn::Layer { public: relative_embeddings_k_module() { one_blob_only = true; } virtual int forward(const ncnn::Mat& bottom_blob, ncnn::Mat& top_blob, const ncnn::Option& opt) const { const int window_size = 4; const int wsize = bottom_blob.w; const int len = bottom_blob.h; const int num_heads = bottom_blob.c; top_blob.create(len, len, num_heads); top_blob.fill(0.f); #pragma omp parallel for num_threads(opt.num_threads) for (int q = 0; q < num_heads; q++) { const ncnn::Mat x0 = bottom_blob.channel(q); ncnn::Mat out0 = top_blob.channel(q); for (int i = 0; i < len; i++) { const float* xptr = x0.row(i) + std::max(0, window_size - i); float* outptr = out0.row(i) + std::max(i - window_size, 0); const int wsize2 = std::min(len, i - window_size + wsize) - std::max(i - window_size, 0); for (int j = 0; j < wsize2; j++) { *outptr++ = *xptr++; } } } return 0; } }; DEFINE_LAYER_CREATOR(relative_embeddings_k_module) class relative_embeddings_v_module : public ncnn::Layer { public: relative_embeddings_v_module() { one_blob_only = true; } virtual int forward(const ncnn::Mat& bottom_blob, ncnn::Mat& top_blob, const ncnn::Option& opt) const { const int window_size = 4; const int wsize = window_size * 2 + 1; const int len = bottom_blob.h; const int num_heads = bottom_blob.c; top_blob.create(wsize, len, num_heads); top_blob.fill(0.f); #pragma omp parallel for num_threads(opt.num_threads) for (int q = 0; q < num_heads; q++) { const ncnn::Mat x0 = bottom_blob.channel(q); ncnn::Mat out0 = top_blob.channel(q); for (int i = 0; i < len; i++) { const float* xptr = x0.row(i) + std::max(i - window_size, 0); float* outptr = out0.row(i) + std::max(0, window_size - i); const int wsize2 = std::min(len, i - window_size + wsize) - std::max(i - window_size, 0); for (int j = 0; j < wsize2; j++) { *outptr++ = *xptr++; } } } return 0; } }; DEFINE_LAYER_CREATOR(relative_embeddings_v_module) class piecewise_rational_quadratic_transform_module : public ncnn::Layer { public: piecewise_rational_quadratic_transform_module() { one_blob_only = false; } virtual int forward(const std::vector& bottom_blobs, std::vector& top_blobs, const ncnn::Option& opt) const { const ncnn::Mat& h = bottom_blobs[0]; const ncnn::Mat& x1 = bottom_blobs[1]; ncnn::Mat& outputs = top_blobs[0]; const int num_bins = 10; const int filter_channels = 192; const bool reverse = true; const float tail_bound = 5.0f; const float DEFAULT_MIN_BIN_WIDTH = 1e-3f; const float DEFAULT_MIN_BIN_HEIGHT = 1e-3f; const float DEFAULT_MIN_DERIVATIVE = 1e-3f; const int batch_size = x1.w; const int h_params_per_item = 2 * num_bins + (num_bins - 1); // 29 outputs = x1.clone(); float* out_ptr = outputs; #pragma omp parallel for num_threads(opt.num_threads) for (int i = 0; i < batch_size; ++i) { const float current_x = ((const float*)x1)[i]; const float* h_data = h.row(i); if (current_x < -tail_bound || current_x > tail_bound) { continue; } std::vector unnormalized_widths(num_bins); std::vector unnormalized_heights(num_bins); std::vector unnormalized_derivatives(num_bins + 1); const float inv_sqrt_filter_channels = 1.0f / sqrtf(filter_channels); for (int j = 0; j < num_bins; ++j) { unnormalized_widths[j] = h_data[j] * inv_sqrt_filter_channels; } for (int j = 0; j < num_bins; ++j) { unnormalized_heights[j] = h_data[num_bins + j] * inv_sqrt_filter_channels; } for (int j = 0; j < num_bins - 1; ++j) { unnormalized_derivatives[j + 1] = h_data[2 * num_bins + j]; } const float constant = logf(expf(1.f - DEFAULT_MIN_DERIVATIVE) - 1.f); unnormalized_derivatives[0] = constant; unnormalized_derivatives[num_bins] = constant; const float left = -tail_bound, right = tail_bound; const float bottom = -tail_bound, top = tail_bound; // Softmax + Affine std::vector widths(num_bins); float w_max = -INFINITY; for (float val : unnormalized_widths) w_max = std::max(w_max, val); float w_sum = 0.f; for (int j = 0; j < num_bins; ++j) { widths[j] = expf(unnormalized_widths[j] - w_max); w_sum += widths[j]; } for (int j = 0; j < num_bins; ++j) { widths[j] = DEFAULT_MIN_BIN_WIDTH + (1.f - DEFAULT_MIN_BIN_WIDTH * num_bins) * (widths[j] / w_sum); } // cumwidths std::vector cumwidths(num_bins + 1); cumwidths[0] = left; float current_w_sum = 0.f; for (int j = 0; j < num_bins - 1; ++j) { current_w_sum += widths[j]; cumwidths[j + 1] = left + (right - left) * current_w_sum; } cumwidths[num_bins] = right; // heights std::vector heights(num_bins); float h_max = -INFINITY; for (float val : unnormalized_heights) h_max = std::max(h_max, val); float h_sum = 0.f; for (int j = 0; j < num_bins; ++j) { heights[j] = expf(unnormalized_heights[j] - h_max); h_sum += heights[j]; } for (int j = 0; j < num_bins; ++j) { heights[j] = DEFAULT_MIN_BIN_HEIGHT + (1.f - DEFAULT_MIN_BIN_HEIGHT * num_bins) * (heights[j] / h_sum); } // cumheights std::vector cumheights(num_bins + 1); cumheights[0] = bottom; float current_h_sum = 0.f; for (int j = 0; j < num_bins - 1; ++j) { current_h_sum += heights[j]; cumheights[j + 1] = bottom + (top - bottom) * current_h_sum; } cumheights[num_bins] = top; // Softplus std::vector derivatives(num_bins + 1); for (int j = 0; j < num_bins + 1; ++j) { float x = unnormalized_derivatives[j]; derivatives[j] = DEFAULT_MIN_DERIVATIVE + (x > 0 ? x + logf(1.f + expf(-x)) : logf(1.f + expf(x))); } // bin_idx int bin_idx = 0; if (reverse) { auto it = std::upper_bound(cumheights.begin(), cumheights.end(), current_x); bin_idx = std::distance(cumheights.begin(), it) - 1; } else { auto it = std::upper_bound(cumwidths.begin(), cumwidths.end(), current_x); bin_idx = std::distance(cumwidths.begin(), it) - 1; } bin_idx = std::max(0, std::min(bin_idx, num_bins - 1)); // collect coeffs const float input_cumwidths = cumwidths[bin_idx]; const float input_bin_widths = cumwidths[bin_idx + 1] - cumwidths[bin_idx]; const float input_cumheights = cumheights[bin_idx]; const float input_heights = cumheights[bin_idx + 1] - cumheights[bin_idx]; const float input_derivatives = derivatives[bin_idx]; const float input_derivatives_plus_one = derivatives[bin_idx + 1]; const float delta = input_heights / input_bin_widths; // apply transform if (reverse) { float a = (current_x - input_cumheights) * (input_derivatives + input_derivatives_plus_one - 2 * delta) + input_heights * (delta - input_derivatives); float b = input_heights * input_derivatives - (current_x - input_cumheights) * (input_derivatives + input_derivatives_plus_one - 2 * delta); float c = -delta * (current_x - input_cumheights); float discriminant = b * b - 4 * a * c; discriminant = std::max(0.f, discriminant); float root = (2 * c) / (-b - sqrtf(discriminant)); out_ptr[i] = root * input_bin_widths + input_cumwidths; } else { float theta = (current_x - input_cumwidths) / input_bin_widths; float theta_one_minus_theta = theta * (1 - theta); float numerator = input_heights * (delta * theta * theta + input_derivatives * theta_one_minus_theta); float denominator = delta + ((input_derivatives + input_derivatives_plus_one - 2 * delta) * theta_one_minus_theta); out_ptr[i] = input_cumheights + numerator / denominator; } } return 0; } }; DEFINE_LAYER_CREATOR(piecewise_rational_quadratic_transform_module) static bool is_word_eos(const char* word) { const char c = word[0]; return c == ',' || c == '.' || c == ';' || c == '?' || c == '!'; } static void find_word_id(const std::map >& dict, const char* word, const unsigned char*& ids) { ids = 0; unsigned char first_char = toupper(word[0]); if (dict.find(first_char) == dict.end()) return; const std::vector& wordlist = dict.at(first_char); for (size_t i = 0; i < wordlist.size(); i++) { if (strcasecmp(wordlist[i], word) == 0) { // hit ids = (const unsigned char*)(wordlist[i] + strlen(wordlist[i]) + 1); return; } } } static void simple_phonemize(const char* text, std::vector& sequence_ids) { // this is a very simple g2p function, it works for english only // load dict buffer std::vector dictbinbuf; { FILE* fp = fopen("en-word_id.bin", "rb"); if (!fp) return; fseek(fp, 0, SEEK_END); size_t len = ftell(fp); rewind(fp); dictbinbuf.resize(len); fread(dictbinbuf.data(), 1, len, fp); fclose(fp); } // build dict std::map > dict; { const unsigned char* p = dictbinbuf.data(); const char* word = (const char*)p; for (size_t i = 0; i < dictbinbuf.size(); i++) { if (dictbinbuf[i] == 0xff) { unsigned int first_char = toupper(word[0]); dict[first_char].push_back(word); word = (const char*)(p + i + 1); } } } // phonemize mainpart { const int ID_PAD = 0; // interleaved const int ID_BOS = 1; // beginning of sentence const int ID_EOS = 2; // end of sentence const int ID_SPACE = 3; // space bool last_char_is_control = false; bool sentence_begin = true; bool sentence_end = true; char word[256]; const char* p = text; while (*p) { if (sentence_end && !last_char_is_control) { sequence_ids.push_back(ID_BOS); sequence_ids.push_back(ID_PAD); sentence_end = false; } if (sentence_begin || last_char_is_control) { // the very first word } else { // space id sequence_ids.push_back(ID_SPACE); sequence_ids.push_back(ID_PAD); } if (isalnum((unsigned char)*p)) { char* pword = word; // alpha or number *pword++ = *p++; // consume word int wordlen = 1; while (isalnum((unsigned char)*p) && wordlen < 233) { *pword++ = *p++; wordlen++; } *pword = '\0'; if (is_word_eos(word)) { if (!sentence_end) sequence_ids.push_back(ID_EOS); sentence_end = true; last_char_is_control = false; sentence_begin = false; continue; } const unsigned char* ids = 0; find_word_id(dict, word, ids); if (ids) { const unsigned char* pids = ids; while (*pids != 0xff) { sequence_ids.push_back(*pids); sequence_ids.push_back(ID_PAD); pids++; } } else { // no such word, spell alphabet one by one char tmp[2] = {'\0', '\0'}; for (size_t i = 0; i < strlen(word); i++) { tmp[0] = word[i]; find_word_id(dict, tmp, ids); if (ids) { const unsigned char* pids = ids; while (*pids != 0xff) { sequence_ids.push_back(*pids); sequence_ids.push_back(ID_PAD); pids++; } if (i + 1 != strlen(word)) { sequence_ids.push_back(ID_SPACE); sequence_ids.push_back(ID_PAD); } } else { fprintf(stderr, "word char %c not recognized\n", word[i]); } } } last_char_is_control = false; sentence_begin = false; continue; } else { // skip control character p++; last_char_is_control = true; } } if (!sentence_end) sequence_ids.push_back(ID_EOS); } } static void path_attention(const ncnn::Mat& logw, const ncnn::Mat& m_p, const ncnn::Mat& logs_p, float noise_scale, float length_scale, ncnn::Mat& z_p) { const int x_lengths = logw.w; // assert m_p.h == logs_p.h const int depth = m_p.h; std::vector w_ceil(x_lengths); int y_lengths = 0; for (int i = 0; i < x_lengths; i++) { w_ceil[i] = (int)ceilf(expf(logw[i]) * length_scale); y_lengths += w_ceil[i]; } z_p.create(y_lengths, depth); for (int i = 0; i < depth; i++) { const float* m_p_ptr = m_p.row(i); const float* logs_p_ptr = logs_p.row(i); float* ptr = z_p.row(i); for (int j = 0; j < x_lengths; j++) { const float m = m_p_ptr[j]; const float nl = expf(logs_p_ptr[j]) * noise_scale; const int duration = w_ceil[j]; for (int k = 0; k < duration; k++) { ptr[k] = m + (rand() / (float)RAND_MAX) * nl; } ptr += duration; } } } static int tts_piper(const char* text, int speaker_id, std::vector& pcm) { // zh models could be found at // https://github.com/nihui/ncnn-android-piper/tree/master/app/src/main/assets // hyper parameters from https://huggingface.co/datasets/rhasspy/piper-checkpoints/blob/main/en/en_US/libritts_r/medium/config.json const float noise_scale = 0.333f; const float length_scale = 1.f; const float noise_scale_w = 0.333f; // phonemize ncnn::Mat sequence; { std::vector sequence_ids; simple_phonemize(text, sequence_ids); const int sequence_length = (int)sequence_ids.size(); sequence.create(sequence_length); memcpy(sequence, sequence_ids.data(), sequence_length * sizeof(int)); } // enc_p ncnn::Mat x; ncnn::Mat m_p; ncnn::Mat logs_p; { ncnn::Net enc_p; enc_p.opt.use_vulkan_compute = true; enc_p.register_custom_layer("piper.train.vits.attentions.relative_embeddings_k_module", relative_embeddings_k_module_layer_creator); enc_p.register_custom_layer("piper.train.vits.attentions.relative_embeddings_v_module", relative_embeddings_v_module_layer_creator); enc_p.load_param("en_enc_p.ncnn.param"); enc_p.load_model("en_enc_p.ncnn.bin"); ncnn::Extractor ex = enc_p.create_extractor(); ex.input("in0", sequence); ex.extract("out0", x); ex.extract("out1", m_p); ex.extract("out2", logs_p); } // emb_g ncnn::Mat g; { ncnn::Net emb_g; emb_g.opt.use_vulkan_compute = true; emb_g.load_param("en_emb_g.ncnn.param"); emb_g.load_model("en_emb_g.ncnn.bin"); ncnn::Mat speaker_id_mat(1); { int* p = speaker_id_mat; p[0] = speaker_id; } ncnn::Extractor ex = emb_g.create_extractor(); ex.input("in0", speaker_id_mat); ex.extract("out0", g); g = g.reshape(1, g.w); } // dp ncnn::Mat logw; { ncnn::Net dp; dp.opt.use_vulkan_compute = true; dp.register_custom_layer("piper.train.vits.modules.piecewise_rational_quadratic_transform_module", piecewise_rational_quadratic_transform_module_layer_creator); dp.load_param("en_dp.ncnn.param"); dp.load_model("en_dp.ncnn.bin"); ncnn::Mat noise(x.w, 2); for (int i = 0; i < noise.w * noise.h; i++) { noise[i] = rand() / (float)RAND_MAX * noise_scale_w; } ncnn::Extractor ex = dp.create_extractor(); ex.input("in0", x); ex.input("in1", noise); ex.input("in2", g); ex.extract("out0", logw); } // path attention ncnn::Mat z_p; { path_attention(logw, m_p, logs_p, noise_scale, length_scale, z_p); } // flow ncnn::Mat z; { ncnn::Net flow; flow.opt.use_vulkan_compute = true; flow.load_param("en_flow.ncnn.param"); flow.load_model("en_flow.ncnn.bin"); ncnn::Extractor ex = flow.create_extractor(); ex.input("in0", z_p); ex.input("in1", g); ex.extract("out0", z); } // dec ncnn::Mat o; { ncnn::Net dec; dec.opt.use_vulkan_compute = true; dec.load_param("en_dec.ncnn.param"); dec.load_model("en_dec.ncnn.bin"); ncnn::Extractor ex = dec.create_extractor(); ex.input("in0", z); ex.input("in1", g); ex.extract("out0", o); } // normalize and clip { float volume = 1.f; float absmax = 0.f; for (int i = 0; i < o.w; i++) { absmax = std::max(absmax, fabs(o[i])); } if (absmax > 1e-8) { for (int i = 0; i < o.w; i++) { float v = o[i] / absmax * volume; v = std::min(std::max(v, -1.f), 1.f); o[i] = v; } } } // 16bit pcm { pcm.resize(o.w); for (int i = 0; i < o.w; i++) { pcm[i] = (short)(o[i] * 32767); } } return 0; } static void save_pcm_to_wav(const char* path, const short* pcm, int num_samples, int sample_rate) { FILE* f = fopen(path, "wb"); if (!f) return; // write wav header { int16_t num_channels = 1; int16_t bits_per_sample = 16; int32_t byte_rate = sample_rate * num_channels * bits_per_sample / 8; int16_t block_align = num_channels * bits_per_sample / 8; int32_t data_chunk_size = num_samples * num_channels * bits_per_sample / 8; int32_t chunk_size = 36 + data_chunk_size; // RIFF header fwrite("RIFF", 1, 4, f); fwrite(&chunk_size, 4, 1, f); fwrite("WAVE", 1, 4, f); // fmt subchunk fwrite("fmt ", 1, 4, f); int32_t subchunk1_size = 16; int16_t audio_format = 1; // PCM fwrite(&subchunk1_size, 4, 1, f); fwrite(&audio_format, 2, 1, f); fwrite(&num_channels, 2, 1, f); fwrite(&sample_rate, 4, 1, f); fwrite(&byte_rate, 4, 1, f); fwrite(&block_align, 2, 1, f); fwrite(&bits_per_sample, 2, 1, f); // data subchunk fwrite("data", 1, 4, f); fwrite(&data_chunk_size, 4, 1, f); } fwrite(pcm, sizeof(short), num_samples, f); fclose(f); } int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "Usage: %s [sentences] [speaker id 0~903] [out path]\n", argv[0]); fprintf(stderr, " %s \"Hello World\" 0 out.wav\n", argv[0]); fprintf(stderr, " %s \"Happy New Year\" 123 out.wav\n", argv[0]); return 0; } const char* text = argv[1]; const int speaker_id = atoi(argv[2]); const char* outpath = argv[3]; std::vector pcm; tts_piper(text, speaker_id, pcm); // "sample_rate": 22050 save_pcm_to_wav(outpath, pcm.data(), pcm.size(), 22050); return 0; }