-
Notifications
You must be signed in to change notification settings - Fork 92
KittenTTS module. #1107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
KittenTTS module. #1107
Changes from 9 commits
de1ef6c
8d7a4db
01cb7d1
2b2a6de
a5ab64f
3a611e6
9f5bf7e
a284a5c
d188bf5
5a8baa9
4a082cc
0249b33
b41356a
acebf8e
b1ce2a2
9d015f4
5198ca7
a8c493d
10bf356
356abac
4272cc6
a794550
b6db9f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| #include <glib/gstdio.h> | ||
|
jsett marked this conversation as resolved.
|
||
| #include <unistd.h> | ||
| #include <stdint.h> | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
| #include <time.h> | ||
| #include <sys/stat.h> | ||
| #include <sys/types.h> | ||
| #include <errno.h> | ||
| #include <curl/curl.h> | ||
| #include <stdbool.h> | ||
| #include <glib.h> | ||
| #include <onnxruntime_c_api.h> | ||
| #include <inttypes.h> | ||
| #include <sndfile.h> | ||
| #include <libxml/parser.h> | ||
| #include <libxml/tree.h> | ||
| #include <espeak-ng/espeak_ng.h> | ||
| #include <espeak-ng/speak_lib.h> | ||
| #include "module_utils.h" | ||
|
|
||
| #include <speechd_types.h> | ||
| #include "spd_module_main.h" | ||
|
|
||
| #define CHECK_STATUS(expr) \ | ||
| do { \ | ||
| OrtStatus* status = (expr); \ | ||
| if (status != NULL) { \ | ||
| const char* msg = g_ort->GetErrorMessage(status); \ | ||
| fprintf(stderr, "ONNX Runtime Error: %s\n", msg); \ | ||
| g_ort->ReleaseStatus(status); \ | ||
| exit(1); \ | ||
| } \ | ||
| } while (0) | ||
|
|
||
| extern int model_type; | ||
| extern int ROWS; | ||
| #define COLS 256 | ||
|
|
||
| #define PAD "$" | ||
| #define PUNCTUATION ";:,.!?¡¿—…\"«»\"\" " | ||
| #define LETTERS "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" | ||
|
jsett marked this conversation as resolved.
|
||
| #define LETTERS_IPA "ɑɐɒæɓʙβɔɕçɗɖðʤəɘɚɛɜɝɞɟʄɡɠɢʛɦɧħɥʜɨɪʝɭɬɫɮʟɱɯɰŋɳɲɴøɵɸθœɶʘɹɺɾɻʀʁɽʂʃʈʧʉʊʋⱱʌɣɤʍχʎʏʑʐʒʔʡʕʢǀǁǂǃˈˌːˑʼʴʰʱʲʷˠˤ˞↓↑→↗↘'̩'ᵻ" | ||
|
|
||
| #define SYMBOLS PAD PUNCTUATION LETTERS LETTERS_IPA | ||
|
|
||
| // setting var's | ||
| extern float speed; | ||
| //['Leo','Kiki','Hugo','Rosie','Bruno','Luna','Jasper','Bella'] | ||
| extern GString *voice; | ||
| extern GString *voice_setting; | ||
| // paths var's | ||
| extern GString *model_path; | ||
| extern GString *voices_path; | ||
| extern const char *home_dir; | ||
| extern GString *model_dir; | ||
|
|
||
| extern bool stop_generation; | ||
|
|
||
| // holds and array of values that must be passed to the model based off the requested voice and length of the text. | ||
| extern float *voice_styles; | ||
|
|
||
| extern const OrtApi* g_ort; | ||
| extern OrtEnv* env; | ||
| extern OrtSessionOptions* session_options; | ||
| extern OrtSession* session; | ||
|
|
||
| #define TARGET_SUBDIR ".config/speech-dispatcher/extra" | ||
|
|
||
| typedef struct { | ||
| const char *url; | ||
| const char *filename; | ||
| curl_off_t expected_size; | ||
| const char *expected_sha256; | ||
| } FileInfo; | ||
|
|
||
| extern const FileInfo FILES[]; | ||
|
|
||
| #define NUM_FILES (sizeof(FILES) / sizeof(FILES[0])) | ||
|
|
||
| #define VOICE_LIST(X) \ | ||
| X(Leo) \ | ||
| X(Kiki) \ | ||
| X(Hugo) \ | ||
| X(Rosie) \ | ||
| X(Bruno) \ | ||
| X(Luna) \ | ||
| X(Jasper) \ | ||
| X(Bella) \ | ||
| X(Leo_Low) \ | ||
| X(Kiki_Low) \ | ||
| X(Hugo_Low) \ | ||
| X(Rosie_Low) \ | ||
| X(Bruno_Low) \ | ||
| X(Luna_Low) \ | ||
| X(Jasper_Low) \ | ||
| X(Bella_Low) \ | ||
| X(Leo_High) \ | ||
| X(Kiki_High) \ | ||
| X(Hugo_High) \ | ||
| X(Rosie_High) \ | ||
| X(Bruno_High) \ | ||
| X(Luna_High) \ | ||
| X(Jasper_High) \ | ||
| X(Bella_High) | ||
|
|
||
| #define DEFINE_VOICE(name_token) static SPDVoice voice_##name_token = { .name = #name_token, .language = "en" }; | ||
| #define VOICE_PTR_ITEM(name_token) &voice_##name_token, | ||
|
|
||
| // kitten_downloader.c | ||
| int download_models(void); | ||
|
|
||
| // kitten_model.c | ||
| int init_voice_style(const char* voices_path); | ||
| void cleanup_voice_style(); | ||
| GArray *get_style(const char *text, const char *voice); | ||
| GArray *get_char_indices(const gchar *locate, const gchar *index_str); | ||
| GString *get_phonemes(const char *text); | ||
| int init_model(const char* model_path); | ||
| void cleanup_model(); | ||
| GArray* run_model(GArray *inputs_array, GArray *styles_array, float speed); | ||
| void convert_float_to_short(const float* in_buffer, GArray* out_buffer, size_t num_samples); | ||
| GArray* kitten_speak(const char* data); | ||
| int reload_models_and_voices(const char *model_filename, const char* voice_filename); | ||
|
|
||
| // kitten_worker.c | ||
| int init_model_thread_pool(); | ||
| int cleanup_threads(); | ||
| int model_change_voice(const char *var, const char *val); | ||
| int model_change_speed(const char *var, const char *val); | ||
| int add_generate_speech_task(const char* data, size_t bytes); | ||
| int model_stop_generation(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is best to be handled by package managers instead? As models update, we don't want to frequently update and recompile this as downstream packages might lag behind for years.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also all the download links are outside of this project's control. Which means that we won't be able to fix a broken download until a new release is packaged by distributions and shipped to the end user (which as commented, might be months/years after).
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It already checks if the models/voices are in the path
I have added the ability to set the download configuration through the models dot conf file. Here is an example dot conf on how to do that https://gist.github.com/jsett/6146eb2803f8780a1830ed816bdc94ef
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm afraid distribution would not organize their packages this way. Like piper, kitten models are general purpose in the sense of the Text-to-Speech task. If we take example from piper, the AUR packages place the models into It is also likely that different distributions wouldn't come to agreement on where these models should be placed. So the distribution packagers would adjust the conf default to accommodate their packaging schemes.
Nevertheless, I personally wouldn't expect program like speech-dispatcher (as a daemon process) to make network requests (and I strongly believe that we should learn from the log4shell disaster where a logging library contains the code path to make network requests) . So adding this to me feels like a scope creep that I would like to push against, as indicated by the first time linking with libcurl.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not opposed to removing the downloader. But there is a trade off here. If you remove it you increase the complexity for users to get things working, if there distro did not include the models with the install. I would be interested in hearing what other people think.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That could be an idea yes. People installing speech-dispatcher by hand would use it, and distribution people would package the voices. People could still use the script to update the voices. Again, if only upstream voices providers could manage these questions instead of just throwing .onnx files at people... There could simply be a voice downloading manager, even independent from speech-dispatcher, that manages updates and whatnot, and speech-dispatcher would just happen to see the voice files show up, and use them.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unfortunately I don't think hugging face models have a way of doing this currently.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based off the above comments, I think this is what I am going to do. With regards to the downloader.
With regards to the search path for the distro's.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It would really just be a matter of documenting it in README or website or whatever, nothing fancy...
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made all those changes in the latest commit. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| #include "kitten.h" | ||
|
|
||
| const FileInfo FILES[] = { | ||
| { | ||
| "https://huggingface.co/KittenML/kitten-tts-micro-0.8/resolve/main/kitten_tts_micro_v0_8.onnx", | ||
| "kitten_tts_micro_v0_8.onnx", | ||
| 41384970, | ||
| "95481626fee1ba70ce683e69c534fc7cb38433c46ce42d3abbeafb4b9f1a4123" | ||
| }, | ||
| { | ||
| "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/main/kitten_tts_mini_v0_8.onnx", | ||
| "kitten_tts_mini_v0_8.onnx", | ||
| 78268016, | ||
| "0f5bbae4fc4800c98dbc544a87ecfa79510de2fb8222db30d12e5bfe9177df91" | ||
| }, | ||
| { | ||
| "https://huggingface.co/KittenML/kitten-tts-nano-0.2/resolve/main/kitten_tts_nano_v0_2.onnx", | ||
| "kitten_tts_nano_v0_2.onnx", | ||
| 23804156, | ||
| "42fa8809db319cd7c4c83b3c501e2313bf90edf610235291cad605e4adcb242d" | ||
| }, | ||
| { | ||
| "https://github.com/jsett/kittenvoices/raw/refs/heads/main/voices_micro.bin", | ||
| "voices_micro.bin", | ||
| 3276800, | ||
| "12ad10f1fcce8a458b5cf79769b8edd4ba0e11e9fb6532fd192c3500b2b37a5d" | ||
| }, | ||
| { | ||
| "https://github.com/jsett/kittenvoices/raw/refs/heads/main/voices_mini.bin", | ||
| "voices_mini.bin", | ||
| 3276800, | ||
| "0e4965b46333db53ce09c73842623bf7055ea62c67f78803cb7ea9c16da6ac2b" | ||
| }, | ||
| { | ||
| "https://github.com/jsett/kittenvoices/raw/refs/heads/main/voices_nano.bin", | ||
| "voices_nano.bin", | ||
| 8192, | ||
| "42a40a24a352a38657d6cb86ceee51bbc2b7780b29e04fb60bcdf959adccea01" | ||
| } | ||
| }; | ||
|
|
||
| // Helper function to recursively create directories | ||
| static int ensure_directory_exists(const char *path) { | ||
| if (g_mkdir_with_parents(path, 0755) != 0) { | ||
| fprintf(stderr, "Error: Failed to create directory '%s': %s\n", path, g_strerror(errno)); | ||
| return -1; | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| // Helper function to check if file exists | ||
| static int file_exists(const char *path) { | ||
| struct stat buffer; | ||
| return (stat(path, &buffer) == 0); | ||
| } | ||
|
|
||
| // Callback for curl to write HTTP data to disk | ||
| static size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) { | ||
| return fwrite(ptr, size, nmemb, stream); | ||
| } | ||
|
|
||
| // Download file with libcurl | ||
| static int download_file(CURL *curl, const char *url, const char *dest_path) { | ||
| FILE *fp = fopen(dest_path, "wb"); | ||
| if (!fp) { | ||
| fprintf(stderr, "Error: Cannot open destination file '%s' for writing: %s\n", dest_path, g_strerror(errno)); | ||
| return -1; | ||
| } | ||
|
|
||
| curl_easy_setopt(curl, CURLOPT_URL, url); | ||
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); | ||
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); | ||
| curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // Follow redirects | ||
| curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); // Fail on HTTP errors (>=400) | ||
|
|
||
| CURLcode res = curl_easy_perform(curl); | ||
| fclose(fp); | ||
|
|
||
| if (res != CURLE_OK) { | ||
| fprintf(stderr, "Error: Download failed for '%s': %s\n", url, curl_easy_strerror(res)); | ||
| remove(dest_path); // Clean up partial download | ||
| return -1; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| // Verify file SHA256 using GLib GChecksum | ||
| static int verify_sha256(const char *filepath, const char *expected_sha256) { | ||
| GMappedFile *mfile = g_mapped_file_new(filepath, FALSE, NULL); | ||
| if (!mfile) { | ||
| fprintf(stderr, "Error: Failed to memory-map file '%s' for SHA256 calculation.\n", filepath); | ||
| return -1; | ||
| } | ||
|
|
||
| gsize length = g_mapped_file_get_length(mfile); | ||
| const gchar *contents = g_mapped_file_get_contents(mfile); | ||
|
|
||
| GChecksum *checksum = g_checksum_new(G_CHECKSUM_SHA256); | ||
| g_checksum_update(checksum, (const guchar *)contents, length); | ||
| const gchar *computed_sha256 = g_checksum_get_string(checksum); | ||
|
|
||
| int match = (g_ascii_strcasecmp(computed_sha256, expected_sha256) == 0); | ||
|
|
||
| if (!match) { | ||
| fprintf(stderr, "Error: Checksum mismatch for '%s'!\n Expected: %s\n Computed: %s\n", | ||
| filepath, expected_sha256, computed_sha256); | ||
| } | ||
|
|
||
| g_checksum_free(checksum); | ||
| g_mapped_file_unref(mfile); | ||
|
|
||
| return match ? 0 : -1; | ||
| } | ||
|
|
||
| // downloads the models and voices if they do not already exist. | ||
| // also verifys using sha256 and checks file size. | ||
| int download_models(void) { | ||
| // Build absolute destination directory path: ~/.config/speech-dispatcher/extra/ | ||
| char *target_dir = g_build_filename(home_dir, TARGET_SUBDIR, NULL); | ||
|
jsett marked this conversation as resolved.
Outdated
|
||
|
|
||
| if (ensure_directory_exists(target_dir) != 0) { | ||
| g_free(target_dir); | ||
| return EXIT_FAILURE; | ||
| } | ||
|
|
||
| if (curl_global_init(CURL_GLOBAL_ALL) != 0) { | ||
| fprintf(stderr, "Error: Failed to initialize libcurl.\n"); | ||
| g_free(target_dir); | ||
| return EXIT_FAILURE; | ||
| } | ||
|
|
||
| CURL *curl = curl_easy_init(); | ||
| if (!curl) { | ||
| fprintf(stderr, "Error: Failed to create libcurl handle.\n"); | ||
| curl_global_cleanup(); | ||
| g_free(target_dir); | ||
| return EXIT_FAILURE; | ||
| } | ||
|
|
||
| int overall_status = EXIT_SUCCESS; | ||
|
|
||
| for (size_t i = 0; i < NUM_FILES; i++) { | ||
| char *full_path = g_build_filename(target_dir, FILES[i].filename, NULL); | ||
|
|
||
| // Check if file already exists | ||
| if (file_exists(full_path)) { | ||
| fprintf(stderr, "Info: File '%s' already exists. Skipping download.\n", FILES[i].filename); | ||
| g_free(full_path); | ||
| continue; | ||
| } | ||
|
|
||
| fprintf(stderr, "Info: Downloading '%s'...\n", FILES[i].filename); | ||
| if (download_file(curl, FILES[i].url, full_path) != 0) { | ||
| fprintf(stderr, "Error: Aborting process due to download error.\n"); | ||
| g_free(full_path); | ||
| overall_status = EXIT_FAILURE; | ||
| break; | ||
| } | ||
|
|
||
| // Verify File Size | ||
| struct stat st; | ||
| if (stat(full_path, &st) != 0) { | ||
| fprintf(stderr, "Error: Could not stat downloaded file '%s'.\n", full_path); | ||
| g_free(full_path); | ||
| overall_status = EXIT_FAILURE; | ||
| break; | ||
| } | ||
|
|
||
| if ((curl_off_t)st.st_size != FILES[i].expected_size) { | ||
| fprintf(stderr, "Error: File size mismatch for '%s'! Expected: %ld bytes, Got: %ld bytes.\n", | ||
| FILES[i].filename, (long)FILES[i].expected_size, (long)st.st_size); | ||
| g_free(full_path); | ||
| overall_status = EXIT_FAILURE; | ||
| break; | ||
| } | ||
|
|
||
| // Verify SHA256 using GLib | ||
| if (verify_sha256(full_path, FILES[i].expected_sha256) != 0) { | ||
| fprintf(stderr, "Error: Integrity check failed for '%s'.\n", FILES[i].filename); | ||
| g_free(full_path); | ||
| overall_status = EXIT_FAILURE; | ||
| break; | ||
| } | ||
|
|
||
| fprintf(stderr, "Info: Successfully downloaded and verified '%s'.\n", FILES[i].filename); | ||
| g_free(full_path); | ||
| } | ||
|
|
||
| // Cleanup resources | ||
| curl_easy_cleanup(curl); | ||
| curl_global_cleanup(); | ||
| g_free(target_dir); | ||
|
|
||
| return overall_status; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.