From 1340dc8ca456293ccdd9b9503a44cc6597e7c372 Mon Sep 17 00:00:00 2001
From: aks <aks@cforge.org>
Date: Mon, 30 Dec 2024 19:42:17 +0400
Subject: [PATCH] added mod_curl_tts

---
 src/mod/asr_tts/mod_curl_tts/Makefile.am      |  11 +
 .../conf/autoload_configs/curl_tts.conf.xml   |  22 ++
 .../mod_curl_tts/conf/dialplan/example.xml    |   8 +
 src/mod/asr_tts/mod_curl_tts/curl.c           | 185 +++++++++++
 src/mod/asr_tts/mod_curl_tts/mod_curl_tts.c   | 302 ++++++++++++++++++
 src/mod/asr_tts/mod_curl_tts/mod_curl_tts.h   |  72 +++++
 src/mod/asr_tts/mod_curl_tts/utils.c          |  80 +++++
 7 files changed, 680 insertions(+)
 create mode 100644 src/mod/asr_tts/mod_curl_tts/Makefile.am
 create mode 100644 src/mod/asr_tts/mod_curl_tts/conf/autoload_configs/curl_tts.conf.xml
 create mode 100644 src/mod/asr_tts/mod_curl_tts/conf/dialplan/example.xml
 create mode 100644 src/mod/asr_tts/mod_curl_tts/curl.c
 create mode 100644 src/mod/asr_tts/mod_curl_tts/mod_curl_tts.c
 create mode 100644 src/mod/asr_tts/mod_curl_tts/mod_curl_tts.h
 create mode 100644 src/mod/asr_tts/mod_curl_tts/utils.c

diff --git a/src/mod/asr_tts/mod_curl_tts/Makefile.am b/src/mod/asr_tts/mod_curl_tts/Makefile.am
new file mode 100644
index 0000000000..22b75838bf
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/Makefile.am
@@ -0,0 +1,11 @@
+
+include $(top_srcdir)/build/modmake.rulesam
+MODNAME=mod_curl_tts
+
+mod_LTLIBRARIES = mod_curl_tts.la
+mod_curl_tts_la_SOURCES  = mod_curl_tts.c utils.c curl.c
+mod_curl_tts_la_CFLAGS   = $(AM_CFLAGS) -I.
+mod_curl_tts_la_LIBADD   = $(switch_builddir)/libfreeswitch.la
+mod_curl_tts_la_LDFLAGS  = -avoid-version -module -no-undefined -shared
+
+$(am_mod_curl_tts_la_OBJECTS): mod_curl_tts.h
diff --git a/src/mod/asr_tts/mod_curl_tts/conf/autoload_configs/curl_tts.conf.xml b/src/mod/asr_tts/mod_curl_tts/conf/autoload_configs/curl_tts.conf.xml
new file mode 100644
index 0000000000..b4520fa11c
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/conf/autoload_configs/curl_tts.conf.xml
@@ -0,0 +1,22 @@
+<configuration name="curl_tts.conf" description="">
+    <settings>
+        <param name="api-url" value="http://127.0.0.1/default_tts" />
+        <!-- <param name="api-key" value="YOUR-API-KEY" /> -->
+
+        <!-- curl settings -->
+        <param name="connect-timeout" value="10" />
+        <param name="request-timeout" value="25" />
+        <param name="file-size-max" value="2097152" />
+        <param name="log-http-errors" value="true" />
+	<param name="voice-name-as-language" value="true" />
+    <!-- <param name="proxy" value="http://proxy:port" /> -->
+    <!-- <param name="proxy-credentials" value="" /> -->
+    <!-- <param name="user-agent" value="Mozilla/1.0" /> -->
+
+        <!-- cache -->
+        <param name="cache-path" value="/tmp/curl-tts-cache" />
+        <param name="cache-enable" value="false" />
+
+    </settings>
+
+</configuration>
diff --git a/src/mod/asr_tts/mod_curl_tts/conf/dialplan/example.xml b/src/mod/asr_tts/mod_curl_tts/conf/dialplan/example.xml
new file mode 100644
index 0000000000..86f9e43ceb
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/conf/dialplan/example.xml
@@ -0,0 +1,8 @@
+
+<extension name="curl-tts">
+    <condition field="destination_number" expression="^(3123)$">
+	<action application="answer"/>
+	<action application="speak" data="curl|en|Hello world!"/>
+	<action application="hangup"/>
+    </condition>
+</extension>
diff --git a/src/mod/asr_tts/mod_curl_tts/curl.c b/src/mod/asr_tts/mod_curl_tts/curl.c
new file mode 100644
index 0000000000..b2fe58c9ea
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/curl.c
@@ -0,0 +1,185 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2005-2014, Anthony Minessale II <anthm@freeswitch.org>
+ *
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * Module Contributor(s):
+ *  Konstantin Alexandrin <akscfx@gmail.com>
+ *
+ *
+ */
+#include "mod_curl_tts.h"
+
+extern globals_t globals;
+
+static size_t curl_io_write_callback(char *buffer, size_t size, size_t nitems, void *user_data) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *)user_data;
+    size_t len = (size * nitems);
+
+    if(len > 0 && tts_ctx->curl_recv_buffer) {
+        switch_buffer_write(tts_ctx->curl_recv_buffer, buffer, len);
+    }
+
+    return len;
+}
+
+static size_t curl_io_read_callback(char *buffer, size_t size, size_t nitems, void *user_data) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *)user_data;
+    size_t nmax = (size * nitems);
+    size_t ncur = (tts_ctx->curl_send_buffer_len > nmax) ? nmax : tts_ctx->curl_send_buffer_len;
+
+    memmove(buffer, tts_ctx->curl_send_buffer_ref, ncur);
+    tts_ctx->curl_send_buffer_ref += ncur;
+    tts_ctx->curl_send_buffer_len -= ncur;
+
+    return ncur;
+}
+
+
+switch_status_t curl_perform(tts_ctx_t *tts_ctx, char *text) {
+    switch_status_t status = SWITCH_STATUS_SUCCESS;
+    CURL *curl_handle = NULL;
+    switch_curl_slist_t *headers = NULL;
+    switch_CURLcode curl_ret = 0;
+    long http_resp = 0;
+    char *pdata = NULL;
+    char *qtext = NULL;
+
+    if(!tts_ctx->api_url) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "api_url not determined\n");
+        return SWITCH_STATUS_FALSE;
+    }
+
+    if(text) {
+        qtext = escape_dquotes(text);
+    }
+
+    if(tts_ctx->curl_params && !switch_core_hash_empty(tts_ctx->curl_params)) {
+        const void *hkey = NULL; void *hval = NULL;
+        switch_hash_index_t *hidx = NULL;
+        cJSON *jopts = NULL;
+
+        jopts = cJSON_CreateObject();
+        for(hidx = switch_core_hash_first_iter(tts_ctx->curl_params, hidx); hidx; hidx = switch_core_hash_next(&hidx)) {
+            switch_core_hash_this(hidx, &hkey, NULL, &hval);
+            if(hkey && hval) {
+                cJSON_AddItemToObject(jopts, (char *)hkey, cJSON_CreateString((char *)hval));
+            }
+        }
+
+        cJSON_AddItemToObject(jopts, "language", cJSON_CreateString((char *)tts_ctx->language));
+        cJSON_AddItemToObject(jopts, "text", cJSON_CreateString((char *)qtext));
+
+        pdata = cJSON_PrintUnformatted(jopts);
+        cJSON_Delete(jopts);
+    } else {
+        cJSON *jopts = NULL;
+
+        jopts = cJSON_CreateObject();
+
+        cJSON_AddItemToObject(jopts, "language", cJSON_CreateString((char *)tts_ctx->language));
+        cJSON_AddItemToObject(jopts, "text", cJSON_CreateString((char *)qtext));
+
+        pdata = cJSON_PrintUnformatted(jopts);
+        cJSON_Delete(jopts);
+    }
+
+    // switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "POST: [%s]\n", pdata);
+
+    tts_ctx->media_ctype = NULL;
+    tts_ctx->curl_send_buffer_len = strlen(pdata);
+    tts_ctx->curl_send_buffer_ref = pdata;
+
+    curl_handle = switch_curl_easy_init();
+
+    headers = switch_curl_slist_append(headers, "Content-Type: application/json; charset=utf-8");
+    headers = switch_curl_slist_append(headers, "Expect:");
+
+    switch_curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers);
+    switch_curl_easy_setopt(curl_handle, CURLOPT_POST, 1);
+    switch_curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
+
+    switch_curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, tts_ctx->curl_send_buffer_len);
+    switch_curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, (void *) pdata);
+    switch_curl_easy_setopt(curl_handle, CURLOPT_READFUNCTION, curl_io_read_callback);
+    switch_curl_easy_setopt(curl_handle, CURLOPT_READDATA, (void *) tts_ctx);
+
+    switch_curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, curl_io_write_callback);
+    switch_curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *) tts_ctx);
+
+    if(globals.connect_timeout > 0) {
+        switch_curl_easy_setopt(curl_handle, CURLOPT_CONNECTTIMEOUT, globals.connect_timeout);
+    }
+    if(globals.request_timeout > 0) {
+        switch_curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, globals.request_timeout);
+    }
+    if(globals.user_agent) {
+        switch_curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, globals.user_agent);
+    }
+
+    if(strncasecmp(tts_ctx->api_url, "https", 5) == 0) {
+        switch_curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0);
+        switch_curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0);
+    }
+    if(globals.proxy) {
+        if(globals.proxy_credentials != NULL) {
+            switch_curl_easy_setopt(curl_handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
+            switch_curl_easy_setopt(curl_handle, CURLOPT_PROXYUSERPWD, globals.proxy_credentials);
+        }
+        if(strncasecmp(globals.proxy, "https", 5) == 0) {
+            switch_curl_easy_setopt(curl_handle, CURLOPT_PROXY_SSL_VERIFYPEER, 0);
+        }
+        switch_curl_easy_setopt(curl_handle, CURLOPT_PROXY, globals.proxy);
+    }
+
+    if(tts_ctx->api_key) {
+        curl_easy_setopt(curl_handle, CURLOPT_XOAUTH2_BEARER, tts_ctx->api_key);
+        curl_easy_setopt(curl_handle, CURLOPT_HTTPAUTH, CURLAUTH_BEARER);
+    }
+
+    switch_curl_easy_setopt(curl_handle, CURLOPT_URL, tts_ctx->api_url);
+
+    curl_ret = switch_curl_easy_perform(curl_handle);
+    if(!curl_ret) {
+        switch_curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &http_resp);
+        if(!http_resp) { switch_curl_easy_getinfo(curl_handle, CURLINFO_HTTP_CONNECTCODE, &http_resp); }
+    } else {
+        http_resp = curl_ret;
+    }
+
+    if(http_resp != 200) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "http-error=[%ld] (%s)\n", http_resp, tts_ctx->api_url);
+        status = SWITCH_STATUS_FALSE;
+    } else {
+        char *ct = NULL;
+        if(!curl_easy_getinfo(curl_handle, CURLINFO_CONTENT_TYPE, &ct) && ct) {
+            tts_ctx->media_ctype = switch_core_strdup(tts_ctx->pool, ct);
+        }
+    }
+
+    if(tts_ctx->curl_recv_buffer) {
+        if(switch_buffer_inuse(tts_ctx->curl_recv_buffer) > 0) {
+            switch_buffer_write(tts_ctx->curl_recv_buffer, "\0", 1);
+        }
+    }
+
+    if(curl_handle) { switch_curl_easy_cleanup(curl_handle); }
+    if(headers) { switch_curl_slist_free_all(headers); }
+
+    switch_safe_free(pdata);
+    switch_safe_free(qtext);
+
+    return status;
+}
+
diff --git a/src/mod/asr_tts/mod_curl_tts/mod_curl_tts.c b/src/mod/asr_tts/mod_curl_tts/mod_curl_tts.c
new file mode 100644
index 0000000000..36ef4e31a1
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/mod_curl_tts.c
@@ -0,0 +1,302 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2005-2014, Anthony Minessale II <anthm@freeswitch.org>
+ *
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * Module Contributor(s):
+ *  Konstantin Alexandrin <akscfx@gmail.com>
+ *
+ * Provides the ability to interact with TTS services over HTTP
+ *
+ * Development repository:
+ * https://github.com/akscf/mod_curl_tts
+ *
+ */
+#include "mod_curl_tts.h"
+
+globals_t globals;
+
+SWITCH_MODULE_LOAD_FUNCTION(mod_curl_tts_load);
+SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_curl_tts_shutdown);
+SWITCH_MODULE_DEFINITION(mod_curl_tts, mod_curl_tts_load, mod_curl_tts_shutdown, NULL);
+
+// ---------------------------------------------------------------------------------------------------------------------------------------------
+// speech api
+// ---------------------------------------------------------------------------------------------------------------------------------------------
+static switch_status_t speech_open(switch_speech_handle_t *sh, const char *voice, int samplerate, int channels, switch_speech_flag_t *flags) {
+    switch_status_t status = SWITCH_STATUS_SUCCESS;
+    tts_ctx_t *tts_ctx = NULL;
+
+    tts_ctx = switch_core_alloc(sh->memory_pool, sizeof(tts_ctx_t));
+    tts_ctx->pool = sh->memory_pool;
+    tts_ctx->fhnd = switch_core_alloc(tts_ctx->pool, sizeof(switch_file_handle_t));
+    tts_ctx->language = (globals.fl_voice_name_as_language && voice) ? switch_core_strdup(sh->memory_pool, voice) : NULL;
+    tts_ctx->samplerate = samplerate;
+    tts_ctx->channels = channels;
+    tts_ctx->api_url = globals.api_url;
+    tts_ctx->api_key = globals.api_key;
+
+    sh->private_info = tts_ctx;
+
+    if((status = switch_buffer_create_dynamic(&tts_ctx->curl_recv_buffer, 1024, 8192, globals.file_size_max)) != SWITCH_STATUS_SUCCESS) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "switch_buffer_create_dynamic() fail\n");
+        goto out;
+    }
+
+    if((status = switch_core_hash_init(&tts_ctx->curl_params)) != SWITCH_STATUS_SUCCESS) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "switch_core_hash_init()\n");
+        switch_goto_status(SWITCH_STATUS_GENERR, out);
+    }
+
+out:
+    return status;
+}
+
+static switch_status_t speech_close(switch_speech_handle_t *sh, switch_speech_flag_t *flags) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *) sh->private_info;
+    assert(tts_ctx != NULL);
+
+    if(switch_test_flag(tts_ctx->fhnd, SWITCH_FILE_OPEN)) {
+        switch_core_file_close(tts_ctx->fhnd);
+    }
+
+    if(tts_ctx->curl_recv_buffer) {
+        switch_buffer_destroy(&tts_ctx->curl_recv_buffer);
+    }
+
+    if(!globals.fl_cache_enabled) {
+        if(tts_ctx->mp3_name) unlink(tts_ctx->mp3_name);
+        if(tts_ctx->wav_name) unlink(tts_ctx->wav_name);
+    }
+
+    if(tts_ctx->curl_params) {
+        switch_core_hash_destroy(&tts_ctx->curl_params);
+    }
+
+    return SWITCH_STATUS_SUCCESS;
+}
+
+static switch_status_t speech_feed_tts(switch_speech_handle_t *sh, char *text, switch_speech_flag_t *flags) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *)sh->private_info;
+    switch_status_t status = SWITCH_STATUS_SUCCESS;
+    char digest[SWITCH_MD5_DIGEST_STRING_SIZE + 1] = { 0 };
+    const void *ptr = NULL;
+    uint32_t recv_len = 0;
+
+    assert(tts_ctx != NULL);
+
+    switch_md5_string(digest, (void *)text, strlen(text));
+    if(!tts_ctx->mp3_name) {
+        tts_ctx->mp3_name = switch_core_sprintf(sh->memory_pool, "%s%s%s.mp3", globals.cache_path, SWITCH_PATH_SEPARATOR, digest);
+    }
+    if(!tts_ctx->wav_name) {
+        tts_ctx->wav_name = switch_core_sprintf(sh->memory_pool, "%s%s%s.wav", globals.cache_path, SWITCH_PATH_SEPARATOR, digest);
+    }
+
+    if(switch_file_exists(tts_ctx->mp3_name, tts_ctx->pool) == SWITCH_STATUS_SUCCESS) {
+        if((status = switch_core_file_open(tts_ctx->fhnd, tts_ctx->mp3_name, tts_ctx->channels, tts_ctx->samplerate,
+                                           (SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT), sh->memory_pool)) != SWITCH_STATUS_SUCCESS) {
+            switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Unable to open file: %s\n", tts_ctx->mp3_name);
+            status = SWITCH_STATUS_FALSE;
+            goto out;
+        }
+    } else if(switch_file_exists(tts_ctx->wav_name, tts_ctx->pool) == SWITCH_STATUS_SUCCESS) {
+        if((status = switch_core_file_open(tts_ctx->fhnd, tts_ctx->wav_name, tts_ctx->channels, tts_ctx->samplerate,
+                                           (SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT), sh->memory_pool)) != SWITCH_STATUS_SUCCESS) {
+            switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Unable to open file: %s\n", tts_ctx->wav_name);
+            status = SWITCH_STATUS_FALSE;
+            goto out;
+        }
+    } else {
+        switch_buffer_zero(tts_ctx->curl_recv_buffer);
+        status = curl_perform(tts_ctx , text);
+        recv_len = switch_buffer_peek_zerocopy(tts_ctx->curl_recv_buffer, &ptr);
+
+        if(status == SWITCH_STATUS_SUCCESS) {
+            char *dst_name = NULL;
+
+            if(strcasecmp(tts_ctx->media_ctype, "audio/mpeg") == 0) {
+                dst_name = tts_ctx->mp3_name;
+            } else if(strcasecmp(tts_ctx->media_ctype, "audio/wav") == 0) {
+                dst_name = tts_ctx->wav_name;
+            } else {
+                status = SWITCH_STATUS_FALSE;
+            }
+
+            if(status == SWITCH_STATUS_SUCCESS) {
+                if((status = write_file(dst_name, (switch_byte_t *)ptr, recv_len)) == SWITCH_STATUS_SUCCESS) {
+                    if((status = switch_core_file_open(tts_ctx->fhnd, dst_name, tts_ctx->channels, tts_ctx->samplerate,
+                                                       (SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT), sh->memory_pool)) != SWITCH_STATUS_SUCCESS) {
+                        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Unable to open file: %s\n", dst_name);
+                        goto out;
+                    }
+                }
+            } else {
+                if(!dst_name) {
+                    switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Unsupported media-type (%s)\n", tts_ctx->media_ctype);
+                } else if(globals.fl_log_http_error) {
+                    if(recv_len > 0) {
+                        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Services response: %s\n", (char *)ptr);
+                    }
+                }
+            }
+        }
+    }
+out:
+    return status;
+}
+
+static switch_status_t speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *data_len, switch_speech_flag_t *flags) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *)sh->private_info;
+    size_t len = (*data_len / sizeof(int16_t));
+
+    assert(tts_ctx != NULL);
+
+    if(tts_ctx->fhnd->file_interface == NULL) {
+        return SWITCH_STATUS_FALSE;
+    }
+
+    if(switch_core_file_read(tts_ctx->fhnd, data, &len) != SWITCH_STATUS_SUCCESS) {
+        switch_core_file_close(tts_ctx->fhnd);
+        return SWITCH_STATUS_FALSE;
+    }
+
+    *data_len = (len * sizeof(int16_t));
+    if(!data_len) {
+        switch_core_file_close(tts_ctx->fhnd);
+        return SWITCH_STATUS_BREAK;
+    }
+
+    return SWITCH_STATUS_SUCCESS;
+}
+
+static void speech_flush_tts(switch_speech_handle_t *sh) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *)sh->private_info;
+
+    assert(tts_ctx != NULL);
+
+    if(tts_ctx->fhnd != NULL && tts_ctx->fhnd->file_interface != NULL) {
+        switch_core_file_close(tts_ctx->fhnd);
+    }
+}
+
+static void speech_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val) {
+    tts_ctx_t *tts_ctx = (tts_ctx_t *)sh->private_info;
+
+    assert(tts_ctx != NULL);
+
+    if(strcasecmp(param, "url") == 0) {
+        if(val) tts_ctx->api_url = switch_core_strdup(sh->memory_pool, val);
+    } else if(strcasecmp(param, "key") == 0) {
+        if(val) tts_ctx->api_key = switch_core_strdup(sh->memory_pool, val);
+    } else if(strcasecmp(param, "language") == 0) {
+        if(val) tts_ctx->api_key = switch_core_strdup(sh->memory_pool, val);
+    } else if(strcasecmp(param, "text") == 0) {
+        // reserved (ignore)
+    } else {
+        if(tts_ctx->curl_params && val) {
+            switch_core_hash_insert(tts_ctx->curl_params, param, switch_core_strdup(sh->memory_pool, val));
+        }
+    }
+}
+
+static void speech_numeric_param_tts(switch_speech_handle_t *sh, char *param, int val) {
+}
+
+static void speech_float_param_tts(switch_speech_handle_t *sh, char *param, double val) {
+}
+
+// ---------------------------------------------------------------------------------------------------------------------------------------------
+// main
+// ---------------------------------------------------------------------------------------------------------------------------------------------
+SWITCH_MODULE_LOAD_FUNCTION(mod_curl_tts_load) {
+    switch_status_t status = SWITCH_STATUS_SUCCESS;
+    switch_xml_t cfg, xml, settings, param;
+    switch_speech_interface_t *speech_interface;
+
+    memset(&globals, 0, sizeof(globals));
+    switch_mutex_init(&globals.mutex, SWITCH_MUTEX_NESTED, pool);
+
+    if((xml = switch_xml_open_cfg(MOD_CONFIG_NAME, &cfg, NULL)) == NULL) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Unable to open configuration: %s\n", MOD_CONFIG_NAME);
+        switch_goto_status(SWITCH_STATUS_GENERR, out);
+    }
+
+    if((settings = switch_xml_child(cfg, "settings"))) {
+        for (param = switch_xml_child(settings, "param"); param; param = param->next) {
+            char *var = (char *) switch_xml_attr_soft(param, "name");
+            char *val = (char *) switch_xml_attr_soft(param, "value");
+
+            if(!strcasecmp(var, "api-url")) {
+                if(val) globals.api_url = switch_core_strdup(pool, val);
+            } else if(!strcasecmp(var, "api-key")) {
+                if(val) globals.api_key = switch_core_strdup(pool, val);
+            } else if(!strcasecmp(var, "cache-path")) {
+                if(val) globals.cache_path = switch_core_strdup(pool, val);
+            } else if(!strcasecmp(var, "user-agent")) {
+                if(val) globals.user_agent = switch_core_strdup(pool, val);
+            } else if(!strcasecmp(var, "request-timeout")) {
+                if(val) globals.request_timeout = atoi(val);
+            } else if(!strcasecmp(var, "connect-timeout")) {
+                if(val) globals.connect_timeout = atoi(val);
+            } else if(!strcasecmp(var, "voice-name-as-language")) {
+                if(val) globals.fl_voice_name_as_language = switch_true(val);
+            } else if(!strcasecmp(var, "log-http-errors")) {
+                if(val) globals.fl_log_http_error = switch_true(val);
+            } else if(!strcasecmp(var, "cache-enable")) {
+                if(val) globals.fl_cache_enabled = switch_true(val);
+            } else if(!strcasecmp(var, "file-size-max")) {
+                if(val) globals.file_size_max = atoi(val);
+            } else if(!strcasecmp(var, "proxy")) {
+                if(val) globals.proxy = switch_core_strdup(pool, val);
+            } else if(!strcasecmp(var, "proxy-credentials")) {
+                if(val) globals.proxy_credentials = switch_core_strdup(pool, val);
+            }
+        }
+    }
+
+    globals.tmp_path = SWITCH_GLOBAL_dirs.temp_dir;
+    globals.cache_path = (globals.cache_path == NULL ? "/tmp/curl-tts-cache" : globals.cache_path);
+    globals.file_size_max = globals.file_size_max > 0 ? globals.file_size_max : FILE_SIZE_MAX;
+
+    if(switch_directory_exists(globals.cache_path, NULL) != SWITCH_STATUS_SUCCESS) {
+        switch_dir_make(globals.cache_path, SWITCH_FPROT_OS_DEFAULT, NULL);
+    }
+
+    *module_interface = switch_loadable_module_create_module_interface(pool, modname);
+    speech_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_SPEECH_INTERFACE);
+    speech_interface->interface_name = "curl";
+
+    speech_interface->speech_open = speech_open;
+    speech_interface->speech_close = speech_close;
+    speech_interface->speech_feed_tts = speech_feed_tts;
+    speech_interface->speech_read_tts = speech_read_tts;
+    speech_interface->speech_flush_tts = speech_flush_tts;
+
+    speech_interface->speech_text_param_tts = speech_text_param_tts;
+    speech_interface->speech_numeric_param_tts = speech_numeric_param_tts;
+    speech_interface->speech_float_param_tts = speech_float_param_tts;
+
+    switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "CURL-TTS (%s)\n", MOD_VERSION);
+out:
+    if(xml) {
+        switch_xml_free(xml);
+    }
+    return status;
+}
+
+SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_curl_tts_shutdown) {
+
+    return SWITCH_STATUS_SUCCESS;
+}
diff --git a/src/mod/asr_tts/mod_curl_tts/mod_curl_tts.h b/src/mod/asr_tts/mod_curl_tts/mod_curl_tts.h
new file mode 100644
index 0000000000..8539852e0f
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/mod_curl_tts.h
@@ -0,0 +1,72 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2005-2014, Anthony Minessale II <anthm@freeswitch.org>
+ *
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * Module Contributor(s):
+ *  Konstantin Alexandrin <akscfx@gmail.com>
+ *
+ *
+ */
+#ifndef MOD_CURL_TTS_H
+#define MOD_CURL_TTS_H
+
+#include <switch.h>
+#include <switch_curl.h>
+
+#define MOD_VERSION         "1.0.0"
+#define MOD_CONFIG_NAME     "curl_tts.conf"
+#define FILE_SIZE_MAX       (2*1024*1024)
+
+typedef struct {
+    switch_mutex_t          *mutex;
+    char                    *cache_path;
+    char                    *tmp_path;
+    char                    *user_agent;
+    char                    *api_url;
+    char                    *api_key;
+    char                    *proxy;
+    char                    *proxy_credentials;
+    uint32_t                file_size_max;
+    uint32_t                request_timeout;            // seconds
+    uint32_t                connect_timeout;            // seconds
+    uint8_t                 fl_voice_name_as_language;
+    uint8_t                 fl_log_http_error;
+    uint8_t                 fl_cache_enabled;
+} globals_t;
+
+typedef struct {
+    switch_memory_pool_t    *pool;
+    switch_file_handle_t    *fhnd;
+    switch_buffer_t         *curl_recv_buffer;
+    switch_hash_t           *curl_params;
+    char                    *curl_send_buffer_ref;
+    char                    *api_url;
+    char                    *api_key;
+    char                    *language;
+    char                    *mp3_name;
+    char                    *wav_name;
+    char                    *media_ctype;
+    uint32_t                samplerate;
+    uint32_t                channels;
+    size_t                  curl_send_buffer_len;
+} tts_ctx_t;
+
+
+char *escape_dquotes(const char *string);
+switch_status_t write_file(char *file_name, switch_byte_t *buf, uint32_t buf_len);
+switch_status_t curl_perform(tts_ctx_t *tts_ctx, char *text);
+
+
+#endif
diff --git a/src/mod/asr_tts/mod_curl_tts/utils.c b/src/mod/asr_tts/mod_curl_tts/utils.c
new file mode 100644
index 0000000000..422cb64a86
--- /dev/null
+++ b/src/mod/asr_tts/mod_curl_tts/utils.c
@@ -0,0 +1,80 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2005-2014, Anthony Minessale II <anthm@freeswitch.org>
+ *
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * Module Contributor(s):
+ *  Konstantin Alexandrin <akscfx@gmail.com>
+ *
+ *
+ */
+#include "mod_curl_tts.h"
+
+char *escape_dquotes(const char *string) {
+    size_t string_len = strlen(string);
+    size_t i;
+    size_t n = 0;
+    size_t dest_len = 0;
+    char *dest;
+
+    dest_len = strlen(string) + 1;
+    for (i = 0; i < string_len; i++) {
+        switch (string[i]) {
+            case '\"': dest_len += 1; break;
+        }
+    }
+
+    dest = (char *) malloc(sizeof(char) * dest_len);
+    switch_assert(dest);
+
+    for (i = 0; i < string_len; i++) {
+        switch (string[i]) {
+            case '\"':
+                dest[n++] = '\\';
+                dest[n++] = '\"';
+            break;
+            default:
+                dest[n++] = string[i];
+        }
+    }
+    dest[n++] = '\0';
+
+    switch_assert(n == dest_len);
+    return dest;
+}
+
+switch_status_t write_file(char *file_name, switch_byte_t *buf, uint32_t buf_len) {
+    switch_status_t status = SWITCH_STATUS_SUCCESS;
+    switch_memory_pool_t *pool = NULL;
+    switch_size_t len = buf_len;
+    switch_file_t *fd = NULL;
+
+    if(switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "switch_core_new_memory_pool() fail\n");
+        switch_goto_status(SWITCH_STATUS_GENERR, out);
+    }
+    if((status = switch_file_open(&fd, file_name, (SWITCH_FOPEN_WRITE | SWITCH_FOPEN_TRUNCATE | SWITCH_FOPEN_CREATE), SWITCH_FPROT_OS_DEFAULT, pool)) != SWITCH_STATUS_SUCCESS) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Open fail: %s\n", file_name);
+        goto out;
+    }
+    if((status = switch_file_write(fd, buf, &len)) != SWITCH_STATUS_SUCCESS) {
+        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Write fail (%s)\n", file_name);
+    }
+    switch_file_close(fd);
+out:
+    if(pool) {
+        switch_core_destroy_memory_pool(&pool);
+    }
+    return status;
+}