mirror of
https://github.com/jambonz/freeswitch-modules.git
synced 2026-01-25 02:08:27 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da2a21f852 | |||
| 9874bf9ae0 | |||
| 33fee93ec7 | |||
| 41aebafd1c | |||
| cfe9cad816 | |||
| 91ea9f3f73 | |||
| a873f074d9 | |||
| bd69d476e7 | |||
| fea51d5ecf | |||
| 83a2d1d730 | |||
| 3f642467eb | |||
| 622cb14957 | |||
| 1e1a51a3d1 | |||
| 4ce95e6d27 | |||
| f0d15c57a2 | |||
| 2bbc18903c | |||
| 4aabb36a64 | |||
| ee488e4621 | |||
| 751d890d9b | |||
| e431c5d159 |
@@ -480,7 +480,7 @@ AudioPipe::AudioPipe(const char* uuid, const char* host, unsigned int port, cons
|
|||||||
}
|
}
|
||||||
AudioPipe::~AudioPipe() {
|
AudioPipe::~AudioPipe() {
|
||||||
if (m_audio_buffer) delete [] m_audio_buffer;
|
if (m_audio_buffer) delete [] m_audio_buffer;
|
||||||
if (m_recv_buf) delete [] m_recv_buf;
|
if (m_recv_buf) free(m_recv_buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioPipe::connect(void) {
|
void AudioPipe::connect(void) {
|
||||||
|
|||||||
@@ -419,7 +419,10 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string strHost(server + offset);
|
std::string strHost(server + offset);
|
||||||
std::regex re("^(.+?):?(\\d+)?(/.*)?$");
|
//- `([^/:]+)` captures the hostname/IP address, match any character except in the set
|
||||||
|
//- `:?([0-9]*)?` optionally captures a colon and the port number, if it's present.
|
||||||
|
//- `(/.*)` captures everything else (the path).
|
||||||
|
std::regex re("([^/:]+):?([0-9]*)?(/.*)?$");
|
||||||
std::smatch matches;
|
std::smatch matches;
|
||||||
if(std::regex_search(strHost, matches, re)) {
|
if(std::regex_search(strHost, matches, re)) {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ mod_azure_tts_la_CFLAGS = $(AM_CFLAGS)
|
|||||||
mod_azure_tts_la_CXXFLAGS = $(AM_CXXFLAGS) -std=c++14 -I/usr/local/include/MicrosoftSpeechSDK/cxx_api -I/usr/local/include/MicrosoftSpeechSDK/c_api
|
mod_azure_tts_la_CXXFLAGS = $(AM_CXXFLAGS) -std=c++14 -I/usr/local/include/MicrosoftSpeechSDK/cxx_api -I/usr/local/include/MicrosoftSpeechSDK/c_api
|
||||||
|
|
||||||
mod_azure_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
mod_azure_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||||
mod_azure_tts_la_LDFLAGS = -avoid-version -module -no-undefined -L/usr/local/lib/MicrosoftSpeechSDK/x64 -lMicrosoft.CognitiveServices.Speech.core -shared `pkg-config --libs boost` -lstdc++
|
mod_azure_tts_la_LDFLAGS = -avoid-version -module -no-undefined -L/usr/local/lib/MicrosoftSpeechSDK/x64 -lMicrosoft.CognitiveServices.Speech.core -shared -lstdc++ -lboost_system -lboost_thread
|
||||||
@@ -17,7 +17,7 @@ using namespace Microsoft::CognitiveServices::Speech;
|
|||||||
|
|
||||||
static std::string fullDirPath;
|
static std::string fullDirPath;
|
||||||
|
|
||||||
static void start_synthesis(std::shared_ptr<SpeechSynthesizer> speechSynthesizer, const char* text) {
|
static void start_synthesis(std::shared_ptr<SpeechSynthesizer> speechSynthesizer, const char* text, azure_t* a) {
|
||||||
try {
|
try {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "start_synthesis calling \n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "start_synthesis calling \n");
|
||||||
auto result = std::strncmp(text, "<speak", 6) == 0 ?
|
auto result = std::strncmp(text, "<speak", 6) == 0 ?
|
||||||
@@ -25,20 +25,24 @@ static void start_synthesis(std::shared_ptr<SpeechSynthesizer> speechSynthesizer
|
|||||||
speechSynthesizer->SpeakTextAsync(text).get();
|
speechSynthesizer->SpeakTextAsync(text).get();
|
||||||
|
|
||||||
if (result->Reason == ResultReason::SynthesizingAudioCompleted) {
|
if (result->Reason == ResultReason::SynthesizingAudioCompleted) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "start_synthesis completed id %s, audio data - bytes: %ld, milliseconds: %ld milliseconds\n",
|
a->response_code = 200;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "start_synthesis completed id %s, audio data - bytes: %ld, duration: %ldms\n",
|
||||||
result->ResultId.c_str(), result->GetAudioLength(), result->AudioDuration.count());
|
result->ResultId.c_str(), result->GetAudioLength(), result->AudioDuration.count());
|
||||||
} else if (result->Reason == ResultReason::Canceled) {
|
} else if (result->Reason == ResultReason::Canceled) {
|
||||||
auto cancellation = SpeechSynthesisCancellationDetails::FromResult(result);
|
auto cancellation = SpeechSynthesisCancellationDetails::FromResult(result);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR,
|
a->response_code = static_cast<long int>(cancellation->ErrorCode);
|
||||||
"Error synthesizing text %s: (%d) %s.\n", text, static_cast<int>(cancellation->ErrorCode), cancellation->ErrorDetails.c_str());
|
a->err_msg = strdup(cancellation->ErrorDetails.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error synthesizing text %d with error string: %s.\n",
|
||||||
|
static_cast<int>(cancellation->ErrorCode), cancellation->ErrorDetails.c_str());
|
||||||
} else {
|
} else {
|
||||||
|
a->response_code = 500;
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error synthsize text %s (%d).\n", text, static_cast<int>(result->Reason));
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error synthsize text %s (%d).\n", text, static_cast<int>(result->Reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
a->response_code = 500;
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "mod_azure_tts: Exception in start_synthesis %s\n", e.what());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "mod_azure_tts: Exception in start_synthesis %s\n", e.what());
|
||||||
}
|
}
|
||||||
|
a->draining = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -48,14 +52,14 @@ extern "C" {
|
|||||||
/* create temp folder for cache files */
|
/* create temp folder for cache files */
|
||||||
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
if (!baseDir) {
|
if (!baseDir) {
|
||||||
baseDir = "/var/";
|
baseDir = "/tmp/";
|
||||||
}
|
}
|
||||||
if (strcmp(baseDir, "/") == 0) {
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
return SWITCH_STATUS_FALSE;
|
return SWITCH_STATUS_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
// Create the directory with read, write, and execute permissions for everyone
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
mode_t oldMask = umask(0);
|
mode_t oldMask = umask(0);
|
||||||
@@ -131,23 +135,12 @@ extern "C" {
|
|||||||
return SWITCH_STATUS_FALSE;
|
return SWITCH_STATUS_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a->session_id) {
|
if (a->rate != 8000 /*Hz*/) {
|
||||||
int err;
|
int err;
|
||||||
switch_codec_implementation_t read_impl;
|
a->resampler = speex_resampler_init(1, 8000, a->rate, SWITCH_RESAMPLE_QUALITY, &err);
|
||||||
|
if (0 != err) {
|
||||||
/* lock and unlock session */
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||||
switch_core_session_t *psession = switch_core_session_locate(a->session_id);
|
return SWITCH_STATUS_FALSE;
|
||||||
switch_core_session_get_read_impl(psession, &read_impl);
|
|
||||||
switch_core_session_rwunlock(psession);
|
|
||||||
|
|
||||||
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
|
||||||
a->samples_rate = samples_per_second;
|
|
||||||
if (samples_per_second != 8000 /*Hz*/) {
|
|
||||||
a->resampler = speex_resampler_init(1, 8000, samples_per_second, SWITCH_RESAMPLE_QUALITY, &err);
|
|
||||||
if (0 != err) {
|
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
|
||||||
return SWITCH_STATUS_FALSE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,87 +168,77 @@ extern "C" {
|
|||||||
speechConfig->SetEndpointId(a->endpointId);
|
speechConfig->SetEndpointId(a->endpointId);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
|
try {
|
||||||
|
auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
|
||||||
|
|
||||||
speechSynthesizer->SynthesisStarted += [a](const SpeechSynthesisEventArgs& e) {
|
speechSynthesizer->SynthesisStarted += [a](const SpeechSynthesisEventArgs& e) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts SynthesisStarted\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts SynthesisStarted\n");
|
||||||
a->response_code = 200;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
speechSynthesizer->Synthesizing += [a](const SpeechSynthesisEventArgs& e) {
|
speechSynthesizer->Synthesizing += [a](const SpeechSynthesisEventArgs& e) {
|
||||||
if (a->flushed) return;
|
if (a->flushed) return;
|
||||||
bool fireEvent = false;
|
bool fireEvent = false;
|
||||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) a->circularBuffer;
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) a->circularBuffer;
|
||||||
|
|
||||||
auto audioData = e.Result->GetAudioData();
|
auto audioData = e.Result->GetAudioData();
|
||||||
if (a->file) {
|
if (a->file) {
|
||||||
fwrite(audioData->data(), 1, audioData->size(), a->file);
|
fwrite(audioData->data(), 1, audioData->size(), a->file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this sort of reinterpretation can be dangerous as a general rule, but in this case we know that the data
|
* this sort of reinterpretation can be dangerous as a general rule, but in this case we know that the data
|
||||||
* is 16-bit PCM, so it's safe to do this and its much faster than copying the data byte by byte
|
* is 16-bit PCM, so it's safe to do this and its much faster than copying the data byte by byte
|
||||||
*/
|
*/
|
||||||
const uint16_t* begin = reinterpret_cast<const uint16_t*>(audioData->data());
|
const uint16_t* begin = reinterpret_cast<const uint16_t*>(audioData->data());
|
||||||
const uint16_t* end = reinterpret_cast<const uint16_t*>(audioData->data() + audioData->size());
|
const uint16_t* end = reinterpret_cast<const uint16_t*>(audioData->data() + audioData->size());
|
||||||
|
|
||||||
/* lock as briefly as possible */
|
/* lock as briefly as possible */
|
||||||
switch_mutex_lock(a->mutex);
|
switch_mutex_lock(a->mutex);
|
||||||
if (cBuffer->capacity() - cBuffer->size() < audioData->size()) {
|
if (cBuffer->capacity() - cBuffer->size() < audioData->size()) {
|
||||||
cBuffer->set_capacity(cBuffer->size() + std::max( audioData->size(), (size_t)BUFFER_SIZE));
|
cBuffer->set_capacity(cBuffer->size() + std::max( audioData->size(), (size_t)BUFFER_SIZE));
|
||||||
}
|
}
|
||||||
cBuffer->insert(cBuffer->end(), begin, end);
|
cBuffer->insert(cBuffer->end(), begin, end);
|
||||||
switch_mutex_unlock(a->mutex);
|
switch_mutex_unlock(a->mutex);
|
||||||
|
|
||||||
if (0 == a->reads++) {
|
if (0 == a->reads++) {
|
||||||
fireEvent = true;
|
fireEvent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fireEvent && a->session_id) {
|
if (fireEvent && a->session_id) {
|
||||||
auto endTime = std::chrono::high_resolution_clock::now();
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
auto startTime = *static_cast<std::chrono::time_point<std::chrono::high_resolution_clock>*>(a->startTime);
|
auto startTime = *static_cast<std::chrono::time_point<std::chrono::high_resolution_clock>*>(a->startTime);
|
||||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||||
auto time_to_first_byte_ms = std::to_string(duration.count());
|
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||||
switch_core_session_t* session = switch_core_session_locate(a->session_id);
|
switch_core_session_t* session = switch_core_session_locate(a->session_id);
|
||||||
if (session) {
|
if (session) {
|
||||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
switch_core_session_rwunlock(session);
|
switch_core_session_rwunlock(session);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switch_event_t *event;
|
switch_event_t *event;
|
||||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||||
switch_channel_event_set_data(channel, event);
|
switch_channel_event_set_data(channel, event);
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||||
if (a->cache_filename) {
|
if (a->cache_filename) {
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", a->cache_filename);
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", a->cache_filename);
|
||||||
|
}
|
||||||
|
switch_event_fire(&event);
|
||||||
|
} else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: failed to create event\n");
|
||||||
}
|
}
|
||||||
switch_event_fire(&event);
|
}else {
|
||||||
} else {
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: channel not found\n");
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: failed to create event\n");
|
|
||||||
}
|
}
|
||||||
}else {
|
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: channel not found\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
speechSynthesizer->SynthesisCompleted += [a](const SpeechSynthesisEventArgs& e) {
|
std::thread(start_synthesis, speechSynthesizer, text, a).detach();
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts SynthesisCompleted\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts sent synthesize request\n");
|
||||||
a->draining = 1;
|
} catch (const std::exception& e) {
|
||||||
};
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_azure_tts: Exception: %s\n", e.what());
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
speechSynthesizer->SynthesisCanceled += [a](const SpeechSynthesisEventArgs& e) {
|
}
|
||||||
if (e.Result->Reason == ResultReason::Canceled) {
|
|
||||||
auto cancellation = SpeechSynthesisCancellationDetails::FromResult(e.Result);
|
|
||||||
a->response_code = static_cast<long int>(cancellation->ErrorCode);
|
|
||||||
a->err_msg = strdup(cancellation->ErrorDetails.c_str());
|
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error synthesizing text %d with error string: %s.\n", static_cast<int>(cancellation->ErrorCode), cancellation->ErrorDetails.c_str());
|
|
||||||
}
|
|
||||||
a->draining = 1;
|
|
||||||
};
|
|
||||||
std::thread(start_synthesis, speechSynthesizer, text).detach();
|
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts sent synthesize request\n");
|
|
||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,9 +265,7 @@ extern "C" {
|
|||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
// azure returned 8000hz 16 bit data, we have to take enough data based on call sample rate.
|
// azure returned 8000hz 16 bit data, we have to take enough data based on call sample rate.
|
||||||
size_t size = a->samples_rate ?
|
size_t size = std::min((*datalen/(2 * a->rate / 8000)), bufSize);
|
||||||
std::min((*datalen/(2 * a->samples_rate / 8000)), bufSize) :
|
|
||||||
std::min((*datalen/2), bufSize);
|
|
||||||
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||||
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||||
switch_mutex_unlock(a->mutex);
|
switch_mutex_unlock(a->mutex);
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ static switch_status_t a_speech_feed_tts(switch_speech_handle_t *sh, char *text,
|
|||||||
a->draining = 0;
|
a->draining = 0;
|
||||||
a->reads = 0;
|
a->reads = 0;
|
||||||
a->flushed = 0;
|
a->flushed = 0;
|
||||||
a->samples_rate = 0;
|
|
||||||
|
|
||||||
return azure_speech_feed_tts(a, text, flags);
|
return azure_speech_feed_tts(a, text, flags);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ typedef struct azure_data {
|
|||||||
int reads;
|
int reads;
|
||||||
int cache_audio;
|
int cache_audio;
|
||||||
int flushed;
|
int flushed;
|
||||||
uint32_t samples_rate;
|
|
||||||
|
|
||||||
void *startTime;
|
void *startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Copyright 2023, Drachtio Communications Services, LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
include $(top_srcdir)/build/modmake.rulesam
|
||||||
|
MODNAME=mod_custom_tts
|
||||||
|
|
||||||
|
mod_LTLIBRARIES = mod_custom_tts.la
|
||||||
|
mod_custom_tts_la_SOURCES = mod_custom_tts.c custom_glue.cpp
|
||||||
|
mod_custom_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||||
|
mod_custom_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||||
|
mod_custom_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared -lstdc++ -lboost_system -lboost_thread -lmpg123
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# mod_custom_tts
|
||||||
|
|
||||||
|
A Freeswitch module that allows speak text to speech audio from custom vendor stream.
|
||||||
@@ -0,0 +1,960 @@
|
|||||||
|
#include "mod_custom_tts.h"
|
||||||
|
#include <switch.h>
|
||||||
|
#include <switch_json.h>
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include <boost/circular_buffer.hpp>
|
||||||
|
#include <boost/thread.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
|
#include <boost/pool/object_pool.hpp>
|
||||||
|
#include <boost/bind/bind.hpp>
|
||||||
|
#include <boost/tokenizer.hpp>
|
||||||
|
#include <boost/foreach.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/assign/list_of.hpp>
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
|
||||||
|
#include "mpg123.h"
|
||||||
|
|
||||||
|
#define BUFFER_GROW_SIZE (8192)
|
||||||
|
#define MP3_DCACHE 8192 * 2
|
||||||
|
|
||||||
|
typedef boost::circular_buffer<uint16_t> CircularBuffer_t;
|
||||||
|
/* Global information, common to all connections */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
CURLM *multi;
|
||||||
|
int still_running;
|
||||||
|
} GlobalInfo_t;
|
||||||
|
static GlobalInfo_t global;
|
||||||
|
|
||||||
|
/* Information associated with a specific easy handle */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
CURL *easy;
|
||||||
|
custom_t* custom;
|
||||||
|
char* body;
|
||||||
|
struct curl_slist *hdr_list;
|
||||||
|
GlobalInfo_t *global;
|
||||||
|
mpg123_handle *mh;
|
||||||
|
char error[CURL_ERROR_SIZE];
|
||||||
|
FILE* file;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> startTime;
|
||||||
|
bool flushed;
|
||||||
|
} ConnInfo_t;
|
||||||
|
|
||||||
|
|
||||||
|
static boost::object_pool<ConnInfo_t> pool ;
|
||||||
|
static std::map<curl_socket_t, boost::asio::ip::tcp::socket *> socket_map;
|
||||||
|
static boost::asio::io_service io_service;
|
||||||
|
static boost::asio::deadline_timer timer(io_service);
|
||||||
|
static std::string fullDirPath;
|
||||||
|
static std::thread worker_thread;
|
||||||
|
|
||||||
|
std::string secondsToMillisecondsString(double seconds) {
|
||||||
|
// Convert to milliseconds
|
||||||
|
double milliseconds = seconds * 1000.0;
|
||||||
|
|
||||||
|
// Truncate to remove fractional part
|
||||||
|
long milliseconds_long = static_cast<long>(milliseconds);
|
||||||
|
|
||||||
|
// Convert to string
|
||||||
|
return std::to_string(milliseconds_long);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURL* createEasyHandle(void) {
|
||||||
|
CURL* easy = curl_easy_init();
|
||||||
|
if(!easy) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "curl_easy_init() failed!\n");
|
||||||
|
return nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_USERAGENT, "jambonz/0.8.5");
|
||||||
|
|
||||||
|
// set connect timeout to 3 seconds and total timeout to 109 seconds
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
|
||||||
|
|
||||||
|
return easy ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cleanupConn(ConnInfo_t *conn) {
|
||||||
|
auto c = conn->custom;
|
||||||
|
|
||||||
|
if (conn->mh) {
|
||||||
|
mpg123_close(conn->mh);
|
||||||
|
mpg123_delete(conn->mh);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( conn->hdr_list ) {
|
||||||
|
curl_slist_free_all(conn->hdr_list);
|
||||||
|
conn->hdr_list = nullptr ;
|
||||||
|
}
|
||||||
|
curl_easy_cleanup(conn->easy);
|
||||||
|
|
||||||
|
if (conn->file) {
|
||||||
|
if (fclose(conn->file) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error closing audio cache file\n");
|
||||||
|
}
|
||||||
|
conn->file = nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
c->conn = nullptr ;
|
||||||
|
c->draining = 1;
|
||||||
|
|
||||||
|
memset(conn, 0, sizeof(ConnInfo_t));
|
||||||
|
pool.destroy(conn) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for completed transfers, and remove their easy handles */
|
||||||
|
void check_multi_info(GlobalInfo_t *g) {
|
||||||
|
CURLMsg *msg;
|
||||||
|
int msgs_left;
|
||||||
|
ConnInfo_t *conn;
|
||||||
|
CURL *easy;
|
||||||
|
CURLcode res;
|
||||||
|
|
||||||
|
while((msg = curl_multi_info_read(g->multi, &msgs_left))) {
|
||||||
|
if(msg->msg == CURLMSG_DONE) {
|
||||||
|
long response_code;
|
||||||
|
double namelookup=0, connect=0, total=0 ;
|
||||||
|
char *ct = NULL ;
|
||||||
|
|
||||||
|
easy = msg->easy_handle;
|
||||||
|
res = msg->data.result;
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &conn);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
|
||||||
|
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_NAMELOOKUP_TIME, &namelookup);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_CONNECT_TIME, &connect);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &total);
|
||||||
|
|
||||||
|
auto c = conn->custom;
|
||||||
|
c->response_code = response_code;
|
||||||
|
if (ct) c->ct = strdup(ct);
|
||||||
|
|
||||||
|
std::string name_lookup_ms = secondsToMillisecondsString(namelookup);
|
||||||
|
std::string connect_ms = secondsToMillisecondsString(connect);
|
||||||
|
std::string final_response_time_ms = secondsToMillisecondsString(total);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
|
||||||
|
"mod_custom_tts: response: %ld, content-type %s,"
|
||||||
|
"dns(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||||
|
"connect(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||||
|
"total(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld\n",
|
||||||
|
response_code, ct,
|
||||||
|
(long)(namelookup), (long)(fmod(namelookup, 1.0) * 1000000),
|
||||||
|
(long)(connect), (long)(fmod(connect, 1.0) * 1000000),
|
||||||
|
(long)(total), (long)(fmod(total, 1.0) * 1000000));
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "name lookup time: %s\n", name_lookup_ms.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "connect time: %s\n", connect_ms.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "final response time: %s\n", final_response_time_ms.c_str());
|
||||||
|
|
||||||
|
c->name_lookup_time_ms = strdup(name_lookup_ms.c_str());
|
||||||
|
c->connect_time_ms = strdup(connect_ms.c_str());
|
||||||
|
c->final_response_time_ms = strdup(final_response_time_ms.c_str());
|
||||||
|
|
||||||
|
curl_multi_remove_handle(g->multi, easy);
|
||||||
|
cleanupConn(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mcode_test(const char *where, CURLMcode code) {
|
||||||
|
if(CURLM_OK != code) {
|
||||||
|
const char *s;
|
||||||
|
switch(code) {
|
||||||
|
case CURLM_CALL_MULTI_PERFORM:
|
||||||
|
s = "CURLM_CALL_MULTI_PERFORM";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_HANDLE:
|
||||||
|
s = "CURLM_BAD_HANDLE";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_EASY_HANDLE:
|
||||||
|
s = "CURLM_BAD_EASY_HANDLE";
|
||||||
|
break;
|
||||||
|
case CURLM_OUT_OF_MEMORY:
|
||||||
|
s = "CURLM_OUT_OF_MEMORY";
|
||||||
|
break;
|
||||||
|
case CURLM_INTERNAL_ERROR:
|
||||||
|
s = "CURLM_INTERNAL_ERROR";
|
||||||
|
break;
|
||||||
|
case CURLM_UNKNOWN_OPTION:
|
||||||
|
s = "CURLM_UNKNOWN_OPTION";
|
||||||
|
break;
|
||||||
|
case CURLM_LAST:
|
||||||
|
s = "CURLM_LAST";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
s = "CURLM_unknown";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_SOCKET:
|
||||||
|
s = "CURLM_BAD_SOCKET";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mcode_test ERROR: %s returns %s:%d\n", where, s, code);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0 ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remsock(int *f, GlobalInfo_t *g) {
|
||||||
|
if(f) {
|
||||||
|
free(f);
|
||||||
|
f = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Called by asio when there is an action on a socket */
|
||||||
|
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp) {
|
||||||
|
int f = *fdp;
|
||||||
|
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X has action %d\n", s, action) ;
|
||||||
|
|
||||||
|
// Socket already POOL REMOVED.
|
||||||
|
if (f == CURL_POLL_REMOVE) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X removed\n", s);
|
||||||
|
remsock(fdp, g);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(socket_map.find(s) == socket_map.end()) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb: socket %#X already closed\n, s");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make sure the event matches what are wanted */
|
||||||
|
if(f == action || f == CURL_POLL_INOUT) {
|
||||||
|
if(error) {
|
||||||
|
action = CURL_CSELECT_ERR;
|
||||||
|
}
|
||||||
|
CURLMcode rc = curl_multi_socket_action(g->multi, s, action, &g->still_running);
|
||||||
|
|
||||||
|
mcode_test("event_cb: curl_multi_socket_action", rc);
|
||||||
|
check_multi_info(g);
|
||||||
|
|
||||||
|
if(g->still_running <= 0) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* keep on watching.
|
||||||
|
* the socket may have been closed and/or fdp may have been changed
|
||||||
|
* in curl_multi_socket_action(), so check them both */
|
||||||
|
if(!error && socket_map.find(s) != socket_map.end() &&
|
||||||
|
(f == action || f == CURL_POLL_INOUT)) {
|
||||||
|
boost::asio::ip::tcp::socket *tcp_socket = socket_map.find(s)->second;
|
||||||
|
|
||||||
|
if(action == CURL_POLL_IN) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
action, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
if(action == CURL_POLL_OUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
action, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* socket functions */
|
||||||
|
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g) {
|
||||||
|
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(s);
|
||||||
|
|
||||||
|
if(it == socket_map.end()) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setsock: socket %#X not found\n, s");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::ip::tcp::socket * tcp_socket = it->second;
|
||||||
|
|
||||||
|
*fdp = act;
|
||||||
|
|
||||||
|
if(act == CURL_POLL_IN) {
|
||||||
|
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(act == CURL_POLL_OUT) {
|
||||||
|
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(act == CURL_POLL_INOUT) {
|
||||||
|
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g) {
|
||||||
|
/* fdp is used to store current action */
|
||||||
|
int *fdp = (int *) calloc(sizeof(int), 1);
|
||||||
|
|
||||||
|
setsock(fdp, s, easy, action, 0, g);
|
||||||
|
curl_multi_assign(g->multi, s, fdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp) {
|
||||||
|
GlobalInfo_t *g = &global;
|
||||||
|
|
||||||
|
int *actionp = (int *) sockp;
|
||||||
|
static const char *whatstr[] = { "none", "IN", "OUT", "INOUT", "REMOVE"};
|
||||||
|
|
||||||
|
if(what == CURL_POLL_REMOVE) {
|
||||||
|
*actionp = what;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(!actionp) {
|
||||||
|
addsock(s, e, what, g);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setsock(actionp, s, e, what, *actionp, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void threadFunc() {
|
||||||
|
/* to make sure the event loop doesn't terminate when there is no work to do */
|
||||||
|
io_service.reset() ;
|
||||||
|
boost::asio::io_service::work work(io_service);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_custom_tts threadFunc - starting\n");
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
io_service.run() ;
|
||||||
|
break ;
|
||||||
|
}
|
||||||
|
catch( std::exception& e) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_custom_tts threadFunc - Error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_custom_tts threadFunc - ending\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Called by asio when our timeout expires */
|
||||||
|
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g)
|
||||||
|
{
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "timer_cb\n");
|
||||||
|
|
||||||
|
if(!error) {
|
||||||
|
CURLMcode rc = curl_multi_socket_action(g->multi, CURL_SOCKET_TIMEOUT, 0, &g->still_running);
|
||||||
|
mcode_test("timer_cb: curl_multi_socket_action", rc);
|
||||||
|
check_multi_info(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g) {
|
||||||
|
|
||||||
|
/* cancel running timer */
|
||||||
|
timer.cancel();
|
||||||
|
|
||||||
|
if(timeout_ms >= 0) {
|
||||||
|
// from libcurl 7.88.1-10+deb12u4 does not allow call curl_multi_socket_action or curl_multi_perform in curl_multi callback directly
|
||||||
|
timer.expires_from_now(boost::posix_time::millisec(timeout_ms ? timeout_ms : 1));
|
||||||
|
timer.async_wait(boost::bind(&timer_cb, boost::placeholders::_1, g));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<uint16_t> convert_mp3_to_linear(ConnInfo_t *conn, uint8_t *data, size_t len) {
|
||||||
|
std::vector<uint16_t> linear_data;
|
||||||
|
int eof = 0;
|
||||||
|
int mp3err = 0;
|
||||||
|
unsigned char decode_buf[MP3_DCACHE];
|
||||||
|
|
||||||
|
if(mpg123_feed(conn->mh, data, len) == MPG123_OK) {
|
||||||
|
while(!eof) {
|
||||||
|
size_t usedlen = 0;
|
||||||
|
off_t frame_offset;
|
||||||
|
unsigned char* audio;
|
||||||
|
|
||||||
|
int decode_status = mpg123_decode_frame(conn->mh, &frame_offset, &audio, &usedlen);
|
||||||
|
|
||||||
|
switch(decode_status) {
|
||||||
|
case MPG123_NEW_FORMAT:
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case MPG123_OK:
|
||||||
|
for(size_t i = 0; i < usedlen; i += 2) {
|
||||||
|
uint16_t value = reinterpret_cast<uint16_t*>(audio)[i / 2];
|
||||||
|
linear_data.push_back(value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MPG123_DONE:
|
||||||
|
case MPG123_NEED_MORE:
|
||||||
|
eof = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MPG123_ERR:
|
||||||
|
default:
|
||||||
|
if(++mp3err >= 5) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Decoder Error!\n");
|
||||||
|
eof = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eof)
|
||||||
|
break;
|
||||||
|
|
||||||
|
mp3err = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return linear_data;
|
||||||
|
}
|
||||||
|
/* CURLOPT_WRITEFUNCTION */
|
||||||
|
static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||||
|
bool fireEvent = false;
|
||||||
|
uint8_t *data = (uint8_t *) ptr;
|
||||||
|
size_t bytes_received = size * nmemb;
|
||||||
|
auto c = conn->custom;
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) c->circularBuffer;
|
||||||
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
|
if (conn->flushed || cBuffer == nullptr) {
|
||||||
|
/* this will abort the transfer */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
switch_mutex_lock(c->mutex);
|
||||||
|
|
||||||
|
if (c->response_code > 0 && c->response_code != 200) {
|
||||||
|
std::string body((char *) ptr, bytes_received);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: received body %s\n", body.c_str());
|
||||||
|
c->err_msg = strdup(body.c_str());
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cache file will stay in the mp3 format for size (smaller) and simplicity */
|
||||||
|
if (conn->file) fwrite(data, sizeof(uint8_t), bytes_received, conn->file);
|
||||||
|
|
||||||
|
pcm_data = convert_mp3_to_linear(conn, data, bytes_received);
|
||||||
|
size_t bytesResampled = pcm_data.size() * sizeof(uint16_t);
|
||||||
|
|
||||||
|
// Resize the buffer if necessary
|
||||||
|
if (cBuffer->capacity() - cBuffer->size() < (bytesResampled / sizeof(uint16_t))) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb growing buffer\n");
|
||||||
|
|
||||||
|
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
|
||||||
|
cBuffer->set_capacity(cBuffer->size() + std::max((bytesResampled / sizeof(uint16_t)), (size_t)BUFFER_GROW_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push the data into the buffer */
|
||||||
|
cBuffer->insert(cBuffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
|
||||||
|
|
||||||
|
if (0 == c->reads++) {
|
||||||
|
fireEvent = true;
|
||||||
|
}
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
}
|
||||||
|
if (fireEvent && c->session_id) {
|
||||||
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - conn->startTime);
|
||||||
|
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||||
|
switch_core_session_t* session = switch_core_session_locate(c->session_id);
|
||||||
|
if (session) {
|
||||||
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
|
if (channel) {
|
||||||
|
switch_event_t *event;
|
||||||
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: firing playback-started\n");
|
||||||
|
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
|
if (c->name_lookup_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_name_lookup_time_ms", c->name_lookup_time_ms);
|
||||||
|
}
|
||||||
|
if (c->connect_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_connect_time_ms", c->connect_time_ms);
|
||||||
|
}
|
||||||
|
if (c->final_response_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_final_response_time_ms", c->final_response_time_ms);
|
||||||
|
}
|
||||||
|
if (c->voice_name) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_voice_name", c->voice_name);
|
||||||
|
}
|
||||||
|
if (c->cache_filename) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", c->cache_filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||||
|
switch_event_fire(&event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", c->session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parseHeader(const std::string& str, std::string& header, std::string& value) {
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
boost::split(parts, str, boost::is_any_of(":"), boost::token_compress_on);
|
||||||
|
|
||||||
|
if (parts.size() != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
header = boost::trim_copy(parts[0]);
|
||||||
|
value = boost::trim_copy(parts[1]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extract_response_code(const std::string& input) {
|
||||||
|
std::size_t space_pos = input.find(' ');
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t code_start_pos = space_pos + 1;
|
||||||
|
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||||
|
if (code_end_pos == std::string::npos) {
|
||||||
|
code_end_pos = input.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||||
|
int response_code = std::stoi(code_str);
|
||||||
|
return response_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||||
|
size_t bytes_received = size * nitems;
|
||||||
|
const std::string prefix = "HTTP/";
|
||||||
|
custom_t* c = conn->custom;
|
||||||
|
std::string header, value;
|
||||||
|
std::string input(buffer, bytes_received);
|
||||||
|
if (parseHeader(input, header, value)) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||||
|
if (input.rfind(prefix, 0) == 0) {
|
||||||
|
try {
|
||||||
|
c->response_code = extract_response_code(input);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", c->response_code);
|
||||||
|
} catch (const std::invalid_argument& e) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_OPENSOCKETFUNCTION */
|
||||||
|
static curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
|
||||||
|
curl_socket_t sockfd = CURL_SOCKET_BAD;
|
||||||
|
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "opensocket: %d\n", purpose);
|
||||||
|
/* restrict to IPv4 */
|
||||||
|
if(purpose == CURLSOCKTYPE_IPCXN && address->family == AF_INET) {
|
||||||
|
/* create a tcp socket object */
|
||||||
|
boost::asio::ip::tcp::socket *tcp_socket = new boost::asio::ip::tcp::socket(io_service);
|
||||||
|
|
||||||
|
/* open it and get the native handle*/
|
||||||
|
boost::system::error_code ec;
|
||||||
|
tcp_socket->open(boost::asio::ip::tcp::v4(), ec);
|
||||||
|
|
||||||
|
if(ec) {
|
||||||
|
/* An error occurred */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't open socket [%ld][%s]\n", ec, ec.message().c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sockfd = tcp_socket->native_handle();
|
||||||
|
|
||||||
|
/* save it for monitoring */
|
||||||
|
socket_map.insert(std::pair<curl_socket_t, boost::asio::ip::tcp::socket *>(sockfd, tcp_socket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_CLOSESOCKETFUNCTION */
|
||||||
|
static int close_socket(void *clientp, curl_socket_t item) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "close_socket : %#X\n", item);
|
||||||
|
|
||||||
|
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(item);
|
||||||
|
if(it != socket_map.end()) {
|
||||||
|
delete it->second;
|
||||||
|
socket_map.erase(it);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
switch_status_t custom_speech_load() {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_loading..\n");
|
||||||
|
memset(&global, 0, sizeof(GlobalInfo_t));
|
||||||
|
global.multi = curl_multi_init();
|
||||||
|
|
||||||
|
if (!global.multi) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "custom_speech_load curl_multi_init() failed, exiting!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_SOCKETFUNCTION, sock_cb);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_SOCKETDATA, &global);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_TIMERDATA, &global);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
||||||
|
|
||||||
|
/* create temp folder for cache files */
|
||||||
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
|
if (!baseDir) {
|
||||||
|
baseDir = "/tmp/";
|
||||||
|
}
|
||||||
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
|
mode_t oldMask = umask(0);
|
||||||
|
int result = mkdir(fullDirPath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
||||||
|
umask(oldMask);
|
||||||
|
if (result != 0) {
|
||||||
|
if (errno != EEXIST) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", fullDirPath.c_str());
|
||||||
|
fullDirPath = "";
|
||||||
|
}
|
||||||
|
else switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "folder %s already exists\n", fullDirPath.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "created folder %s\n", fullDirPath.c_str());
|
||||||
|
}
|
||||||
|
// init mgp123
|
||||||
|
if (mpg123_init() != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to initiate MPG123");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* start worker thread that handles transfers*/
|
||||||
|
std::thread t(threadFunc) ;
|
||||||
|
worker_thread.swap( t ) ;
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_loaded..\n");
|
||||||
|
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t custom_speech_unload() {
|
||||||
|
/* stop the ASIO IO service */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: stopping io service\n");
|
||||||
|
io_service.stop();
|
||||||
|
|
||||||
|
/* Join the worker thread */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: wait for worker thread to complete\n");
|
||||||
|
if (worker_thread.joinable()) {
|
||||||
|
worker_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cleanup curl multi handle*/
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: release curl multi\n");
|
||||||
|
curl_multi_cleanup(global.multi);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: completed\n");
|
||||||
|
|
||||||
|
mpg123_exit();
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t custom_speech_open(custom_t* custom) {
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t custom_speech_feed_tts(custom_t* c, char* text, switch_speech_flag_t *flags) {
|
||||||
|
CURLMcode rc;
|
||||||
|
|
||||||
|
const int MAX_CHARS = 20;
|
||||||
|
char tempText[MAX_CHARS + 4]; // +4 for the ellipsis and null terminator
|
||||||
|
|
||||||
|
if (strlen(text) > MAX_CHARS) {
|
||||||
|
strncpy(tempText, text, MAX_CHARS);
|
||||||
|
strcpy(tempText + MAX_CHARS, "...");
|
||||||
|
} else {
|
||||||
|
strcpy(tempText, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open cache file */
|
||||||
|
if (c->cache_audio && fullDirPath.length() > 0) {
|
||||||
|
switch_uuid_t uuid;
|
||||||
|
char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1];
|
||||||
|
char outfile[512] = "";
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
switch_uuid_get(&uuid);
|
||||||
|
switch_uuid_format(uuid_str, &uuid);
|
||||||
|
|
||||||
|
switch_snprintf(outfile, sizeof(outfile), "%s%s%s.mp3", fullDirPath.c_str(), SWITCH_PATH_SEPARATOR, uuid_str);
|
||||||
|
c->cache_filename = strdup(outfile);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "writing audio cache file to %s\n", c->cache_filename);
|
||||||
|
|
||||||
|
mode_t oldMask = umask(0);
|
||||||
|
fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||||
|
umask(oldMask);
|
||||||
|
if (fd == -1 ) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c->file = fdopen(fd, "wb");
|
||||||
|
if (!c->file) {
|
||||||
|
close(fd);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c->custom_tts_url) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "custom_speech_feed_tts: no custom_tts_url provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* format url*/
|
||||||
|
std::string url = c->custom_tts_url;
|
||||||
|
|
||||||
|
/* create the JSON body */
|
||||||
|
cJSON * jResult = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(jResult, "text", text);
|
||||||
|
cJSON_AddStringToObject(jResult, "type", std::strncmp(text, "<speak", 6) == 0 ? "ssml" : "text");
|
||||||
|
if (c->language) {
|
||||||
|
cJSON_AddStringToObject(jResult, "language", c->language);
|
||||||
|
}
|
||||||
|
if (c->voice_name) {
|
||||||
|
cJSON_AddStringToObject(jResult, "voice", c->voice_name);
|
||||||
|
}
|
||||||
|
char *json = cJSON_PrintUnformatted(jResult);
|
||||||
|
|
||||||
|
cJSON_Delete(jResult);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_feed_tts: [%s] [%s]\n", url.c_str(), tempText);
|
||||||
|
|
||||||
|
ConnInfo_t *conn = pool.malloc() ;
|
||||||
|
|
||||||
|
// COnfigure MPG123
|
||||||
|
int mhError = 0;
|
||||||
|
mpg123_handle *mh = mpg123_new("auto", &mhError);
|
||||||
|
if (!mh) {
|
||||||
|
const char *mhErr = mpg123_plain_strerror(mhError);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error allocating mpg123 handle! %s\n", switch_str_nil(mhErr));
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpg123_open_feed(mh) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpg123_format_all(mh) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpg123_param(mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing single channel!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
CURL* easy = createEasyHandle();
|
||||||
|
c->conn = (void *) conn ;
|
||||||
|
conn->custom = c;
|
||||||
|
conn->easy = easy;
|
||||||
|
conn->mh = mh;
|
||||||
|
conn->global = &global;
|
||||||
|
conn->hdr_list = NULL ;
|
||||||
|
conn->file = c->file;
|
||||||
|
conn->body = json;
|
||||||
|
conn->flushed = false;
|
||||||
|
|
||||||
|
|
||||||
|
c->circularBuffer = (void *) new CircularBuffer_t(8192);
|
||||||
|
|
||||||
|
if (mpg123_param(mh, MPG123_FORCE_RATE, c->rate /*Hz*/, 0) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, conn);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, conn->error);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_PRIVATE, conn);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HEADERDATA, conn);
|
||||||
|
|
||||||
|
/* call this function to get a socket */
|
||||||
|
curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, opensocket);
|
||||||
|
|
||||||
|
/* call this function to close a socket */
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CLOSESOCKETFUNCTION, close_socket);
|
||||||
|
|
||||||
|
if (c->auth_token) {
|
||||||
|
std::ostringstream api_key_stream;
|
||||||
|
api_key_stream << "Authorization: Bearer " << c->auth_token;
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, api_key_stream.str().c_str());
|
||||||
|
}
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, "Accept: audio/mpeg");
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, "Content-Type: application/json");
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, conn->hdr_list);
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, conn->body);
|
||||||
|
//curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, body.length());
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||||
|
|
||||||
|
rc = curl_multi_add_handle(global.multi, conn->easy);
|
||||||
|
mcode_test("new_conn: curl_multi_add_handle", rc);
|
||||||
|
|
||||||
|
/* start a timer to measure the duration until we receive first byte of audio */
|
||||||
|
conn->startTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "custom_speech_feed_tts: called curl_multi_add_handle\n");
|
||||||
|
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t custom_speech_read_tts(custom_t* c, void *data, size_t *datalen, switch_speech_flag_t *flags) {
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) c->circularBuffer;
|
||||||
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
|
{
|
||||||
|
switch_mutex_lock(c->mutex);
|
||||||
|
ConnInfo_t *conn = (ConnInfo_t *) c->conn;
|
||||||
|
if (c->response_code > 0 && c->response_code != 200) {
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_read_tts, returning failure\n") ;
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (conn && conn->flushed) {
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
return SWITCH_STATUS_BREAK;
|
||||||
|
}
|
||||||
|
if (cBuffer->empty()) {
|
||||||
|
if (c->draining) {
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
return SWITCH_STATUS_BREAK;
|
||||||
|
}
|
||||||
|
/* no audio available yet so send silence */
|
||||||
|
memset(data, 255, *datalen);
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
size_t size = std::min((*datalen/2), cBuffer->size());
|
||||||
|
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||||
|
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||||
|
switch_mutex_unlock(c->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(data, pcm_data.data(), pcm_data.size() * sizeof(uint16_t));
|
||||||
|
*datalen = pcm_data.size() * sizeof(uint16_t);
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t custom_speech_flush_tts(custom_t* c) {
|
||||||
|
bool download_complete = c->response_code == 200;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "custom_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
||||||
|
ConnInfo_t *conn = (ConnInfo_t *) c->conn;
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) c->circularBuffer;
|
||||||
|
delete cBuffer;
|
||||||
|
c->circularBuffer = nullptr ;
|
||||||
|
|
||||||
|
if (conn) {
|
||||||
|
conn->flushed = true;
|
||||||
|
if (!download_complete) {
|
||||||
|
if (conn->file) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "closing audio cache file %s because download was interrupted\n", c->cache_filename);
|
||||||
|
if (fclose(conn->file) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "error closing audio cache file\n");
|
||||||
|
}
|
||||||
|
conn->file = nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c->cache_filename) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing audio cache file %s because download was interrupted\n", c->cache_filename);
|
||||||
|
if (unlink(c->cache_filename) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error removing audio cache file %s: %d:%s\n",
|
||||||
|
c->cache_filename, errno, strerror(errno));
|
||||||
|
}
|
||||||
|
free(c->cache_filename);
|
||||||
|
c->cache_filename = nullptr ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c->session_id) {
|
||||||
|
switch_core_session_t* session = switch_core_session_locate(c->session_id);
|
||||||
|
if (session) {
|
||||||
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
|
if (channel) {
|
||||||
|
switch_event_t *event;
|
||||||
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_response_code", std::to_string(c->response_code).c_str());
|
||||||
|
if (c->cache_filename && c->response_code == 200) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", c->cache_filename);
|
||||||
|
}
|
||||||
|
if (c->response_code != 200 && c->err_msg) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_error", c->err_msg);
|
||||||
|
}
|
||||||
|
switch_event_fire(&event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t custom_speech_close(custom_t* c) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "custom_speech_close\n") ;
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef __CUSTOM_GLUE_H__
|
||||||
|
#define __CUSTOM_GLUE_H__
|
||||||
|
|
||||||
|
switch_status_t custom_speech_load();
|
||||||
|
switch_status_t custom_speech_open(custom_t* custom);
|
||||||
|
switch_status_t custom_speech_feed_tts(custom_t* custom, char* text, switch_speech_flag_t *flags);
|
||||||
|
switch_status_t custom_speech_read_tts(custom_t* custom, void *data, size_t *datalen, switch_speech_flag_t *flags);
|
||||||
|
switch_status_t custom_speech_flush_tts(custom_t* custom);
|
||||||
|
switch_status_t custom_speech_close(custom_t* custom);
|
||||||
|
switch_status_t custom_speech_unload();
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#include "mod_custom_tts.h"
|
||||||
|
#include "custom_glue.h"
|
||||||
|
|
||||||
|
SWITCH_MODULE_LOAD_FUNCTION(mod_custom_tts_load);
|
||||||
|
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_custom_tts_shutdown);
|
||||||
|
SWITCH_MODULE_DEFINITION(mod_custom_tts, mod_custom_tts_load, mod_custom_tts_shutdown, NULL);
|
||||||
|
|
||||||
|
static void clearCustomVendor(custom_t* c, int freeAll) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "clearCustomVendor\n");
|
||||||
|
if (c->auth_token) free(c->auth_token);
|
||||||
|
if (c->custom_tts_url) free(c->custom_tts_url);
|
||||||
|
if (c->language) free(c->language);
|
||||||
|
if (c->ct) free(c->ct);
|
||||||
|
if (c->err_msg) free(c->err_msg);
|
||||||
|
if (c->name_lookup_time_ms) free(c->name_lookup_time_ms);
|
||||||
|
if (c->connect_time_ms) free(c->connect_time_ms);
|
||||||
|
if (c->final_response_time_ms) free(c->final_response_time_ms);
|
||||||
|
if (c->cache_filename) free(c->cache_filename);
|
||||||
|
|
||||||
|
|
||||||
|
c->auth_token = NULL;
|
||||||
|
c->custom_tts_url = NULL;
|
||||||
|
c->language = NULL;
|
||||||
|
c->ct = NULL;
|
||||||
|
c->err_msg = NULL;
|
||||||
|
c->name_lookup_time_ms = NULL;
|
||||||
|
c->connect_time_ms = NULL;
|
||||||
|
c->final_response_time_ms = NULL;
|
||||||
|
c->cache_filename = NULL;
|
||||||
|
|
||||||
|
if (freeAll) {
|
||||||
|
if (c->voice_name) free(c->voice_name);
|
||||||
|
if (c->session_id) free(c->session_id);
|
||||||
|
c->voice_name = NULL;
|
||||||
|
c->session_id = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static custom_t * createOrRetrievePrivateData(switch_speech_handle_t *sh) {
|
||||||
|
custom_t *c = (custom_t *) sh->private_info;
|
||||||
|
if (!c) {
|
||||||
|
c = switch_core_alloc(sh->memory_pool, sizeof(*c));
|
||||||
|
sh->private_info = c;
|
||||||
|
memset(c, 0, sizeof(*c));
|
||||||
|
switch_mutex_init(&c->mutex, SWITCH_MUTEX_NESTED, sh->memory_pool);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "allocated custom_t\n");
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t w_speech_open(switch_speech_handle_t *sh, const char *voice_name, int rate, int channels, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
custom_t *c = createOrRetrievePrivateData(sh);
|
||||||
|
c->voice_name = strdup(voice_name);
|
||||||
|
c->rate = rate;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_speech_open voice: %s, rate %d, channels %d\n", voice_name, rate, channels);
|
||||||
|
return custom_speech_open(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static switch_status_t w_speech_close(switch_speech_handle_t *sh, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
switch_status_t rc;
|
||||||
|
custom_t *c = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_close\n");
|
||||||
|
|
||||||
|
switch_mutex_destroy(c->mutex);
|
||||||
|
|
||||||
|
rc = custom_speech_close(c);
|
||||||
|
clearCustomVendor(c, 1);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeswitch will call this function to feed us text to speak
|
||||||
|
*/
|
||||||
|
static switch_status_t w_speech_feed_tts(switch_speech_handle_t *sh, char *text, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
custom_t *c = createOrRetrievePrivateData(sh);
|
||||||
|
c->draining = 0;
|
||||||
|
c->reads = 0;
|
||||||
|
c->response_code = 0;
|
||||||
|
c->err_msg = NULL;
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_feed_tts\n");
|
||||||
|
|
||||||
|
return custom_speech_feed_tts(c, text, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeswitch calls periodically to get some rendered audio in L16 format. We can provide up to 8k of audio at a time.
|
||||||
|
*/
|
||||||
|
static switch_status_t w_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
custom_t *c = createOrRetrievePrivateData(sh);
|
||||||
|
return custom_speech_read_tts(c, data, datalen, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called at the end, not sure exactly what we need to do here..
|
||||||
|
*/
|
||||||
|
static void w_speech_flush_tts(switch_speech_handle_t *sh)
|
||||||
|
{
|
||||||
|
custom_t *c = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_flush_tts\n");
|
||||||
|
custom_speech_flush_tts(c);
|
||||||
|
|
||||||
|
clearCustomVendor(c, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void w_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
||||||
|
{
|
||||||
|
custom_t *c = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_text_param_tts: %s=%s\n", param, val);
|
||||||
|
if (0 == strcmp(param, "auth_token")) {
|
||||||
|
if (c->auth_token) free(c->auth_token);
|
||||||
|
c->auth_token = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "custom_tts_url")) {
|
||||||
|
if (c->custom_tts_url) free(c->custom_tts_url);
|
||||||
|
c->custom_tts_url = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "language")) {
|
||||||
|
if (c->language) free(c->language);
|
||||||
|
c->language = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "session-uuid")) {
|
||||||
|
if (c->session_id) free(c->session_id);
|
||||||
|
c->session_id = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "write_cache_file") && switch_true(val)) {
|
||||||
|
c->cache_audio = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void w_numeric_param_tts(switch_speech_handle_t *sh, char *param, int val)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
static void w_float_param_tts(switch_speech_handle_t *sh, char *param, double val)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SWITCH_MODULE_LOAD_FUNCTION(mod_custom_tts_load)
|
||||||
|
{
|
||||||
|
switch_speech_interface_t *speech_interface;
|
||||||
|
|
||||||
|
*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 = "custom";
|
||||||
|
speech_interface->speech_open = w_speech_open;
|
||||||
|
speech_interface->speech_close = w_speech_close;
|
||||||
|
speech_interface->speech_feed_tts = w_speech_feed_tts;
|
||||||
|
speech_interface->speech_read_tts = w_speech_read_tts;
|
||||||
|
speech_interface->speech_flush_tts = w_speech_flush_tts;
|
||||||
|
speech_interface->speech_text_param_tts = w_text_param_tts;
|
||||||
|
speech_interface->speech_numeric_param_tts = w_numeric_param_tts;
|
||||||
|
speech_interface->speech_float_param_tts = w_float_param_tts;
|
||||||
|
return custom_speech_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_custom_tts_shutdown)
|
||||||
|
{
|
||||||
|
return custom_speech_unload();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef __MOD_CUSTOM_TTS_H__
|
||||||
|
#define __MOD_CUSTOM_TTS_H__
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
typedef struct custom_data {
|
||||||
|
char *voice_name;
|
||||||
|
char *auth_token;
|
||||||
|
char *custom_tts_url;
|
||||||
|
char *language;
|
||||||
|
|
||||||
|
/* result data */
|
||||||
|
long response_code;
|
||||||
|
char *ct;
|
||||||
|
char *name_lookup_time_ms;
|
||||||
|
char *connect_time_ms;
|
||||||
|
char *final_response_time_ms;
|
||||||
|
char *err_msg;
|
||||||
|
char *cache_filename;
|
||||||
|
char *session_id;
|
||||||
|
|
||||||
|
int rate;
|
||||||
|
int draining;
|
||||||
|
int reads;
|
||||||
|
int cache_audio;
|
||||||
|
|
||||||
|
void *conn;
|
||||||
|
void *circularBuffer;
|
||||||
|
switch_mutex_t *mutex;
|
||||||
|
FILE *file;
|
||||||
|
} custom_t;
|
||||||
|
#endif
|
||||||
@@ -112,6 +112,8 @@ namespace {
|
|||||||
tech_pvt->resampler = NULL;
|
tech_pvt->resampler = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NB: do not destroy the mutex here, that is caller responsibility
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if (tech_pvt->vad) {
|
if (tech_pvt->vad) {
|
||||||
switch_vad_destroy(&tech_pvt->vad);
|
switch_vad_destroy(&tech_pvt->vad);
|
||||||
@@ -295,9 +297,20 @@ namespace {
|
|||||||
break;
|
break;
|
||||||
case deepgram::AudioPipe::CONNECTION_DROPPED:
|
case deepgram::AudioPipe::CONNECTION_DROPPED:
|
||||||
// first thing: we can no longer access the AudioPipe
|
// first thing: we can no longer access the AudioPipe
|
||||||
tech_pvt->pAudioPipe = nullptr;
|
|
||||||
tech_pvt->responseHandler(session, TRANSCRIBE_EVENT_DISCONNECT, NULL, tech_pvt->bugname, finished);
|
/**
|
||||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "connection (%s) dropped from far end\n", tech_pvt->bugname);
|
* this is a bit tricky. If we just closed a previos connection it may be returning final transcripts
|
||||||
|
* and then a close event here as it is shutting down (in the reaper thread above).
|
||||||
|
* In this scenario, the fact that the connection is dropped is not significant.
|
||||||
|
*/
|
||||||
|
if (finished) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "old connection (%s) gracefully closed by Deepgram\n", tech_pvt->bugname);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tech_pvt->pAudioPipe = nullptr;
|
||||||
|
tech_pvt->responseHandler(session, TRANSCRIBE_EVENT_DISCONNECT, NULL, tech_pvt->bugname, finished);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "connection (%s) dropped from far end\n", tech_pvt->bugname);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case deepgram::AudioPipe::CONNECTION_CLOSED_GRACEFULLY:
|
case deepgram::AudioPipe::CONNECTION_CLOSED_GRACEFULLY:
|
||||||
// first thing: we can no longer access the AudioPipe
|
// first thing: we can no longer access the AudioPipe
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ mod_LTLIBRARIES = mod_deepgram_tts.la
|
|||||||
mod_deepgram_tts_la_SOURCES = mod_deepgram_tts.c deepgram_glue.cpp
|
mod_deepgram_tts_la_SOURCES = mod_deepgram_tts.c deepgram_glue.cpp
|
||||||
mod_deepgram_tts_la_CFLAGS = $(AM_CFLAGS)
|
mod_deepgram_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||||
mod_deepgram_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
mod_deepgram_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||||
mod_deepgram_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared `pkg-config --libs boost` -lstdc++
|
mod_deepgram_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared -lstdc++ -lboost_system -lboost_thread
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
auto d = conn->deepgram;
|
auto d = conn->deepgram;
|
||||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
||||||
|
|
||||||
if (conn->flushed) {
|
if (conn->flushed || cBuffer == nullptr) {
|
||||||
/* this will abort the transfer */
|
/* this will abort the transfer */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -461,6 +461,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
switch_core_session_t* session = switch_core_session_locate(d->session_id);
|
switch_core_session_t* session = switch_core_session_locate(d->session_id);
|
||||||
if (session) {
|
if (session) {
|
||||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switch_event_t *event;
|
switch_event_t *event;
|
||||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||||
@@ -469,8 +470,14 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: firing playback-started\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: firing playback-started\n");
|
||||||
|
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
if (d->reported_latency) {
|
if (d->reported_model_name) {
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_deepgram_reported_latency_ms", d->reported_latency);
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_deepgram_reported_model_name", d->reported_model_name);
|
||||||
|
}
|
||||||
|
if (d->reported_model_uuid) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_deepgram_reported_model_uuid", d->reported_model_uuid);
|
||||||
|
}
|
||||||
|
if (d->reported_char_count) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_deepgram_reported_char_count", d->reported_char_count);
|
||||||
}
|
}
|
||||||
if (d->request_id) {
|
if (d->request_id) {
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_deepgram_request_id", d->request_id);
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_deepgram_request_id", d->request_id);
|
||||||
@@ -501,7 +508,6 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
}
|
}
|
||||||
switch_core_session_rwunlock(session);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", d->session_id);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", d->session_id);
|
||||||
@@ -522,22 +528,43 @@ static bool parseHeader(const std::string& str, std::string& header, std::string
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int extract_response_code(const std::string& input) {
|
||||||
|
std::size_t space_pos = input.find(' ');
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t code_start_pos = space_pos + 1;
|
||||||
|
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||||
|
if (code_end_pos == std::string::npos) {
|
||||||
|
code_end_pos = input.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||||
|
int response_code = std::stoi(code_str);
|
||||||
|
return response_code;
|
||||||
|
}
|
||||||
|
|
||||||
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||||
size_t bytes_received = size * nitems;
|
size_t bytes_received = size * nitems;
|
||||||
const std::string prefix = "HTTP/2 ";
|
const std::string prefix = "HTTP/";
|
||||||
deepgram_t* d = conn->deepgram;
|
deepgram_t* d = conn->deepgram;
|
||||||
std::string header, value;
|
std::string header, value;
|
||||||
std::string input(buffer, bytes_received);
|
std::string input(buffer, bytes_received);
|
||||||
if (parseHeader(input, header, value)) {
|
if (parseHeader(input, header, value)) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||||
if (0 == header.compare("dg-request-id")) d->request_id = strdup(value.c_str());
|
if (0 == header.compare("dg-request-id")) d->request_id = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("dg-model-name")) d->reported_model_name = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("dg-model-uuid")) d->reported_model_uuid = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("dg-char-count")) d->reported_char_count = strdup(value.c_str());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s\n", input.c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||||
if (input.rfind(prefix, 0) == 0) {
|
if (input.rfind(prefix, 0) == 0) {
|
||||||
try {
|
try {
|
||||||
d->response_code = std::stoi(input.substr(prefix.length()));
|
d->response_code = extract_response_code(input);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "parsed response code: %ld\n", d->response_code);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", d->response_code);
|
||||||
} catch (const std::invalid_argument& e) {
|
} catch (const std::invalid_argument& e) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||||
}
|
}
|
||||||
@@ -607,14 +634,14 @@ extern "C" {
|
|||||||
/* create temp folder for cache files */
|
/* create temp folder for cache files */
|
||||||
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
if (!baseDir) {
|
if (!baseDir) {
|
||||||
baseDir = "/var/";
|
baseDir = "/tmp/";
|
||||||
}
|
}
|
||||||
if (strcmp(baseDir, "/") == 0) {
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
return SWITCH_STATUS_FALSE;
|
return SWITCH_STATUS_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
// Create the directory with read, write, and execute permissions for everyone
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
mode_t oldMask = umask(0);
|
mode_t oldMask = umask(0);
|
||||||
@@ -737,7 +764,9 @@ extern "C" {
|
|||||||
conn->file = d->file;
|
conn->file = d->file;
|
||||||
conn->body = json;
|
conn->body = json;
|
||||||
conn->flushed = false;
|
conn->flushed = false;
|
||||||
|
conn->has_last_byte = false;
|
||||||
|
conn->last_byte = 0;
|
||||||
|
|
||||||
|
|
||||||
d->circularBuffer = (void *) new CircularBuffer_t(BUFFER_GROW_SIZE);
|
d->circularBuffer = (void *) new CircularBuffer_t(BUFFER_GROW_SIZE);
|
||||||
// Always use deepgram at rate 8000 for helping cache audio from jambonz.
|
// Always use deepgram at rate 8000 for helping cache audio from jambonz.
|
||||||
@@ -888,6 +917,7 @@ extern "C" {
|
|||||||
switch_core_session_t* session = switch_core_session_locate(d->session_id);
|
switch_core_session_t* session = switch_core_session_locate(d->session_id);
|
||||||
if (session) {
|
if (session) {
|
||||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switch_event_t *event;
|
switch_event_t *event;
|
||||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||||
@@ -909,7 +939,6 @@ extern "C" {
|
|||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
}
|
}
|
||||||
switch_core_session_rwunlock(session);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ static void cleardeepgram(deepgram_t* d, int freeAll) {
|
|||||||
if (d->api_key) free(d->api_key);
|
if (d->api_key) free(d->api_key);
|
||||||
|
|
||||||
if (d->request_id) free(d->request_id);
|
if (d->request_id) free(d->request_id);
|
||||||
if (d->reported_latency) free(d->reported_latency);
|
if (d->reported_model_name) free(d->reported_model_name);
|
||||||
|
if (d->reported_model_uuid) free(d->reported_model_uuid);
|
||||||
|
if (d->reported_char_count) free(d->reported_char_count);
|
||||||
if (d->ct) free(d->ct);
|
if (d->ct) free(d->ct);
|
||||||
if (d->err_msg) free(d->err_msg);
|
if (d->err_msg) free(d->err_msg);
|
||||||
if (d->name_lookup_time_ms) free(d->name_lookup_time_ms);
|
if (d->name_lookup_time_ms) free(d->name_lookup_time_ms);
|
||||||
@@ -22,7 +24,9 @@ static void cleardeepgram(deepgram_t* d, int freeAll) {
|
|||||||
d->api_key = NULL;
|
d->api_key = NULL;
|
||||||
d->request_id = NULL;
|
d->request_id = NULL;
|
||||||
|
|
||||||
d->reported_latency = NULL;
|
d->reported_model_name = NULL;
|
||||||
|
d->reported_model_uuid = NULL;
|
||||||
|
d->reported_char_count = NULL;
|
||||||
d->ct = NULL;
|
d->ct = NULL;
|
||||||
d->err_msg = NULL;
|
d->err_msg = NULL;
|
||||||
d->name_lookup_time_ms = NULL;
|
d->name_lookup_time_ms = NULL;
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ typedef struct deepgram_data {
|
|||||||
/* result data */
|
/* result data */
|
||||||
long response_code;
|
long response_code;
|
||||||
char *ct;
|
char *ct;
|
||||||
char *reported_latency;
|
// Deepgram hedaers
|
||||||
|
//dg-model-name
|
||||||
|
char *reported_model_name;
|
||||||
|
//dg-model-uuid
|
||||||
|
char *reported_model_uuid;
|
||||||
|
//dg-char-count
|
||||||
|
char *reported_char_count;
|
||||||
|
//dg-request-id
|
||||||
char *request_id;
|
char *request_id;
|
||||||
char *name_lookup_time_ms;
|
char *name_lookup_time_ms;
|
||||||
char *connect_time_ms;
|
char *connect_time_ms;
|
||||||
|
|||||||
@@ -4,6 +4,127 @@
|
|||||||
#include <switch_json.h>
|
#include <switch_json.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
|
|
||||||
|
switch_status_t custom_vendor_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||||
|
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||||
|
std::string auth_token;
|
||||||
|
std::string voice_name;
|
||||||
|
std::string custom_tts_url;
|
||||||
|
std::string language;
|
||||||
|
|
||||||
|
for (const auto& pair : params) {
|
||||||
|
if (pair.first == "auth_token") {
|
||||||
|
auth_token = pair.second;
|
||||||
|
} else if (pair.first == "voice") {
|
||||||
|
voice_name = pair.second;
|
||||||
|
} else if (pair.first == "custom_tts_url") {
|
||||||
|
custom_tts_url = pair.second;
|
||||||
|
} else if (pair.first == "language") {
|
||||||
|
language = pair.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (custom_tts_url.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "custom_vendor_parse_text: no custom_tts_url provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = custom_tts_url;
|
||||||
|
|
||||||
|
/* create the JSON body */
|
||||||
|
cJSON * jResult = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(jResult, "text", text.c_str());
|
||||||
|
cJSON_AddStringToObject(jResult, "type", text.substr(0, 6) == "<speak" ? "ssml" : "text");
|
||||||
|
cJSON_AddNumberToObject(jResult, "samplingRate", 8000);
|
||||||
|
if (!voice_name.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "voice", voice_name.c_str());
|
||||||
|
}
|
||||||
|
if (!language.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "language", language.c_str());
|
||||||
|
}
|
||||||
|
char* _body = cJSON_PrintUnformatted(jResult);
|
||||||
|
body = _body;
|
||||||
|
|
||||||
|
cJSON_Delete(jResult);
|
||||||
|
free(_body);
|
||||||
|
|
||||||
|
// Create headers
|
||||||
|
if (!auth_token.empty()) {
|
||||||
|
headers.push_back("Authorization: Bearer " + auth_token);
|
||||||
|
}
|
||||||
|
headers.push_back("Accept: audio/mp3");
|
||||||
|
headers.push_back("Content-Type: application/json");
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||||
|
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||||
|
std::string api_key;
|
||||||
|
std::string voice_name;
|
||||||
|
std::string model_id;
|
||||||
|
std::string speed_alpha;
|
||||||
|
std::string reduce_latency;
|
||||||
|
|
||||||
|
for (const auto& pair : params) {
|
||||||
|
if (pair.first == "api_key") {
|
||||||
|
api_key = pair.second;
|
||||||
|
} else if (pair.first == "voice") {
|
||||||
|
voice_name = pair.second;
|
||||||
|
} else if (pair.first == "model_id") {
|
||||||
|
model_id = pair.second;
|
||||||
|
} else if (pair.first == "speed_alpha") {
|
||||||
|
speed_alpha = pair.second;
|
||||||
|
} else if (pair.first == "reduce_latency") {
|
||||||
|
reduce_latency = pair.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api_key.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "rimelabs_parse_text: no api_key provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (voice_name.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "rimelabs_parse_text: no voice_name provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (model_id.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "rimelabs_parse_text: no model_id provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "https://users.rime.ai/v1/rime-tts";
|
||||||
|
|
||||||
|
/* create the JSON body */
|
||||||
|
cJSON * jResult = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(jResult, "text", text.c_str());
|
||||||
|
cJSON_AddNumberToObject(jResult, "samplingRate", 8000);
|
||||||
|
if (!voice_name.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "speaker", voice_name.c_str());
|
||||||
|
}
|
||||||
|
if (!model_id.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "modelId", model_id.c_str());
|
||||||
|
}
|
||||||
|
if (!speed_alpha.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "speedAlpha", std::strtof(speed_alpha.c_str(), nullptr));
|
||||||
|
}
|
||||||
|
if (!reduce_latency.empty()) {
|
||||||
|
cJSON_AddBoolToObject(jResult, "reduceLatency", !strcmp(reduce_latency.c_str(), "true") ? 1 : 0);
|
||||||
|
}
|
||||||
|
char* _body = cJSON_PrintUnformatted(jResult);
|
||||||
|
body = _body;
|
||||||
|
|
||||||
|
cJSON_Delete(jResult);
|
||||||
|
free(_body);
|
||||||
|
|
||||||
|
// Create headers
|
||||||
|
headers.push_back("Authorization: Bearer " + api_key);
|
||||||
|
headers.push_back("Accept: audio/mp3");
|
||||||
|
headers.push_back("Content-Type: application/json");
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
switch_status_t whisper_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
switch_status_t whisper_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||||
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||||
std::string api_key;
|
std::string api_key;
|
||||||
@@ -184,6 +305,114 @@ switch_status_t deepgram_parse_text(const std::map<std::string, std::string>& pa
|
|||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||||
|
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||||
|
|
||||||
|
std::string api_key;
|
||||||
|
std::string voice_name;
|
||||||
|
std::string user_id;
|
||||||
|
std::string quality;
|
||||||
|
std::string speed;
|
||||||
|
std::string seed;
|
||||||
|
std::string temperature;
|
||||||
|
std::string voice_engine;
|
||||||
|
std::string emotion;
|
||||||
|
std::string voice_guidance;
|
||||||
|
std::string style_guidance;
|
||||||
|
std::string text_guidance;
|
||||||
|
|
||||||
|
for (const auto& pair : params) {
|
||||||
|
if (pair.first == "api_key") {
|
||||||
|
api_key = pair.second;
|
||||||
|
} else if (pair.first == "voice") {
|
||||||
|
voice_name = pair.second;
|
||||||
|
} else if (pair.first == "user_id") {
|
||||||
|
user_id = pair.second;
|
||||||
|
} else if (pair.first == "quality") {
|
||||||
|
quality = pair.second;
|
||||||
|
} else if (pair.first == "speed") {
|
||||||
|
speed = pair.second;
|
||||||
|
} else if (pair.first == "seed") {
|
||||||
|
seed = pair.second;
|
||||||
|
} else if (pair.first == "temperature") {
|
||||||
|
temperature = pair.second;
|
||||||
|
} else if (pair.first == "voice_engine") {
|
||||||
|
voice_engine = pair.second;
|
||||||
|
} else if (pair.first == "emotion") {
|
||||||
|
emotion = pair.second;
|
||||||
|
} else if (pair.first == "voice_guidance") {
|
||||||
|
voice_guidance = pair.second;
|
||||||
|
} else if (pair.first == "style_guidance") {
|
||||||
|
style_guidance = pair.second;
|
||||||
|
} else if (pair.first == "text_guidance") {
|
||||||
|
text_guidance = pair.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api_key.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "playht_parse_text: no api_key provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (user_id.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "playht_parse_text: no user_id provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (voice_name.empty()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "playht_parse_text: no voice_name provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL
|
||||||
|
url = "https://api.play.ht/api/v2/tts/stream";
|
||||||
|
|
||||||
|
/* create the JSON body */
|
||||||
|
cJSON * jResult = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(jResult, "text", text.c_str());
|
||||||
|
cJSON_AddStringToObject(jResult, "voice", voice_name.c_str());
|
||||||
|
cJSON_AddStringToObject(jResult, "output_format", "mp3");
|
||||||
|
cJSON_AddNumberToObject(jResult, "sample_rate", 8000);
|
||||||
|
if (!voice_engine.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "voice_engine", voice_engine.c_str());
|
||||||
|
}
|
||||||
|
if (!quality.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "quality", quality.c_str());
|
||||||
|
}
|
||||||
|
if (!speed.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "speed", atoi(speed.c_str()));
|
||||||
|
}
|
||||||
|
if (!seed.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "seed", atoi(seed.c_str()));
|
||||||
|
}
|
||||||
|
if (!temperature.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "temperature", std::strtof(temperature.c_str(), nullptr));
|
||||||
|
}
|
||||||
|
if (!emotion.empty()) {
|
||||||
|
cJSON_AddStringToObject(jResult, "emotion", emotion.c_str());
|
||||||
|
}
|
||||||
|
if (!voice_guidance.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "voice_guidance", atoi(voice_guidance.c_str()));
|
||||||
|
}
|
||||||
|
if (!style_guidance.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "style_guidance", atoi(style_guidance.c_str()));
|
||||||
|
}
|
||||||
|
if (!text_guidance.empty()) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "text_guidance", atoi(text_guidance.c_str()));
|
||||||
|
}
|
||||||
|
char* _body = cJSON_PrintUnformatted(jResult);
|
||||||
|
body = _body;
|
||||||
|
|
||||||
|
cJSON_Delete(jResult);
|
||||||
|
free(_body);
|
||||||
|
|
||||||
|
// Create headers
|
||||||
|
headers.push_back("AUTHORIZATION: " + api_key);
|
||||||
|
headers.push_back("X-USER-ID: " + user_id);
|
||||||
|
headers.push_back("Accept: audio/mpeg");
|
||||||
|
headers.push_back("Content-Type: application/json");
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
switch_status_t elevenlabs_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
switch_status_t elevenlabs_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||||
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||||
|
|
||||||
@@ -297,7 +526,13 @@ switch_status_t tts_vendor_parse_text(const std::string& say, std::string& url,
|
|||||||
return azure_parse_text(params, text, url, body, headers, proxy);
|
return azure_parse_text(params, text, url, body, headers, proxy);
|
||||||
} else if (params["vendor"] == "whisper") {
|
} else if (params["vendor"] == "whisper") {
|
||||||
return whisper_parse_text(params, text, url, body, headers);
|
return whisper_parse_text(params, text, url, body, headers);
|
||||||
} else {
|
} else if (params["vendor"] == "playht") {
|
||||||
|
return playht_parse_text(params, text, url, body, headers);
|
||||||
|
} else if (params["vendor"] == "rimelabs") {
|
||||||
|
return rimelabs_parse_text(params, text, url, body, headers);
|
||||||
|
} else if (params["vendor"] == "custom") {
|
||||||
|
return custom_vendor_parse_text(params, text, url, body, headers);
|
||||||
|
} else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "tts_vendor_parse_text: There is no available parser for vendor %s\n", params["vendor"]);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "tts_vendor_parse_text: There is no available parser for vendor %s\n", params["vendor"]);
|
||||||
return SWITCH_STATUS_FALSE;
|
return SWITCH_STATUS_FALSE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) el->circularBuffer;
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) el->circularBuffer;
|
||||||
std::vector<uint16_t> pcm_data;
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
if (conn->flushed) {
|
if (conn->flushed || cBuffer == nullptr) {
|
||||||
/* this will abort the transfer */
|
/* this will abort the transfer */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -552,9 +552,28 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
return bytes_received;
|
return bytes_received;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int extract_response_code(const std::string& input) {
|
||||||
|
std::size_t space_pos = input.find(' ');
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t code_start_pos = space_pos + 1;
|
||||||
|
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||||
|
if (code_end_pos == std::string::npos) {
|
||||||
|
code_end_pos = input.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||||
|
int response_code = std::stoi(code_str);
|
||||||
|
return response_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||||
size_t bytes_received = size * nitems;
|
size_t bytes_received = size * nitems;
|
||||||
const std::string prefix = "HTTP/2 ";
|
const std::string prefix = "HTTP/ ";
|
||||||
elevenlabs_t* el = conn->elevenlabs;
|
elevenlabs_t* el = conn->elevenlabs;
|
||||||
std::string header, value;
|
std::string header, value;
|
||||||
std::string input(buffer, bytes_received);
|
std::string input(buffer, bytes_received);
|
||||||
@@ -565,11 +584,11 @@ static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo
|
|||||||
else if (0 == header.compare("history-item-id")) el->history_item_id = strdup(value.c_str());
|
else if (0 == header.compare("history-item-id")) el->history_item_id = strdup(value.c_str());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s\n", input.c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||||
if (input.rfind(prefix, 0) == 0) {
|
if (input.rfind(prefix, 0) == 0) {
|
||||||
try {
|
try {
|
||||||
el->response_code = std::stoi(input.substr(prefix.length()));
|
el->response_code = extract_response_code(input);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "parsed response code: %ld\n", el->response_code);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", el->response_code);
|
||||||
} catch (const std::invalid_argument& e) {
|
} catch (const std::invalid_argument& e) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||||
}
|
}
|
||||||
@@ -662,14 +681,14 @@ extern "C" {
|
|||||||
/* create temp folder for cache files */
|
/* create temp folder for cache files */
|
||||||
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
if (!baseDir) {
|
if (!baseDir) {
|
||||||
baseDir = "/var/";
|
baseDir = "/tmp/";
|
||||||
}
|
}
|
||||||
if (strcmp(baseDir, "/") == 0) {
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
return SWITCH_STATUS_FALSE;
|
return SWITCH_STATUS_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
// Create the directory with read, write, and execute permissions for everyone
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
mode_t oldMask = umask(0);
|
mode_t oldMask = umask(0);
|
||||||
@@ -822,7 +841,6 @@ extern "C" {
|
|||||||
CURL* easy = createEasyHandle();
|
CURL* easy = createEasyHandle();
|
||||||
|
|
||||||
el->conn = (void *) conn ;
|
el->conn = (void *) conn ;
|
||||||
el->sample_rate = 0;
|
|
||||||
conn->elevenlabs = el;
|
conn->elevenlabs = el;
|
||||||
conn->easy = easy;
|
conn->easy = easy;
|
||||||
conn->global = &global;
|
conn->global = &global;
|
||||||
@@ -833,20 +851,12 @@ extern "C" {
|
|||||||
|
|
||||||
el->circularBuffer = (void *) new CircularBuffer_t(8192);
|
el->circularBuffer = (void *) new CircularBuffer_t(8192);
|
||||||
|
|
||||||
if (el->session_id) {
|
if (el->rate != 8000 /*Hz*/) {
|
||||||
int err;
|
int err;
|
||||||
switch_codec_implementation_t read_impl;
|
el->resampler = speex_resampler_init(1, 8000, el->rate, SWITCH_RESAMPLE_QUALITY, &err);
|
||||||
switch_core_session_t *psession = switch_core_session_locate(el->session_id);
|
if (0 != err) {
|
||||||
switch_core_session_get_read_impl(psession, &read_impl);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||||
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
return SWITCH_STATUS_FALSE;
|
||||||
el->sample_rate = samples_per_second;
|
|
||||||
// elevenlabs output is PCMU 8000
|
|
||||||
if (samples_per_second != 8000 /*Hz*/) {
|
|
||||||
el->resampler = speex_resampler_init(1, 8000, samples_per_second, SWITCH_RESAMPLE_QUALITY, &err);
|
|
||||||
if (0 != err) {
|
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
|
||||||
return SWITCH_STATUS_FALSE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,9 +930,7 @@ extern "C" {
|
|||||||
switch_mutex_unlock(el->mutex);
|
switch_mutex_unlock(el->mutex);
|
||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
size_t size = el->sample_rate ?
|
size_t size = std::min((*datalen/(2 * el->rate / 8000)), cBuffer->size());
|
||||||
std::min((*datalen/(2 * el->sample_rate / 8000)), cBuffer->size()) :
|
|
||||||
std::min((*datalen/2), cBuffer->size());
|
|
||||||
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||||
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||||
switch_mutex_unlock(el->mutex);
|
switch_mutex_unlock(el->mutex);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ struct elevenlabs_data {
|
|||||||
char *cache_filename;
|
char *cache_filename;
|
||||||
|
|
||||||
int rate;
|
int rate;
|
||||||
uint32_t sample_rate;
|
|
||||||
|
|
||||||
void *conn;
|
void *conn;
|
||||||
FILE *file;
|
FILE *file;
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
|||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "grpc_read_thread: error %s (%d)\n", status.message().c_str(), status.code()) ;
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "grpc_read_thread: error %s (%d)\n", status.message().c_str(), status.code()) ;
|
||||||
cJSON* json = cJSON_CreateObject();
|
cJSON* json = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(json, "type", "error");
|
cJSON_AddStringToObject(json, "type", "error");
|
||||||
|
cJSON_AddStringToObject(json, "error_cause", "stream_response");
|
||||||
cJSON_AddStringToObject(json, "error", status.message().c_str());
|
cJSON_AddStringToObject(json, "error", status.message().c_str());
|
||||||
char* jsonString = cJSON_PrintUnformatted(json);
|
char* jsonString = cJSON_PrintUnformatted(json);
|
||||||
cb->responseHandler(session, jsonString, cb->bugname);
|
cb->responseHandler(session, jsonString, cb->bugname);
|
||||||
@@ -339,9 +340,10 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
|||||||
cb->responseHandler(session, "no_audio", cb->bugname);
|
cb->responseHandler(session, "no_audio", cb->bugname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (status.error_code() != 0) {
|
||||||
cJSON* json = cJSON_CreateObject();
|
cJSON* json = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(json, "type", "error");
|
cJSON_AddStringToObject(json, "type", "error");
|
||||||
|
cJSON_AddStringToObject(json, "error_cause", "stream_close");
|
||||||
cJSON_AddItemToObject(json, "error_code", cJSON_CreateNumber(status.error_code()));
|
cJSON_AddItemToObject(json, "error_code", cJSON_CreateNumber(status.error_code()));
|
||||||
cJSON_AddStringToObject(json, "error_message", status.error_message().c_str());
|
cJSON_AddStringToObject(json, "error_message", status.error_message().c_str());
|
||||||
char* jsonString = cJSON_PrintUnformatted(json);
|
char* jsonString = cJSON_PrintUnformatted(json);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ using google::cloud::speech::v2::SpeechRecognitionAlternative;
|
|||||||
using google::cloud::speech::v2::PhraseSet;
|
using google::cloud::speech::v2::PhraseSet;
|
||||||
using google::cloud::speech::v2::PhraseSet_Phrase;
|
using google::cloud::speech::v2::PhraseSet_Phrase;
|
||||||
using google::cloud::speech::v2::StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE;
|
using google::cloud::speech::v2::StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE;
|
||||||
|
using google::cloud::speech::v2::StreamingRecognizeResponse_SpeechEventType_SPEECH_ACTIVITY_BEGIN;
|
||||||
|
using google::cloud::speech::v2::StreamingRecognizeResponse_SpeechEventType_SPEECH_ACTIVITY_END;
|
||||||
using google::cloud::speech::v2::ExplicitDecodingConfig_AudioEncoding_LINEAR16;
|
using google::cloud::speech::v2::ExplicitDecodingConfig_AudioEncoding_LINEAR16;
|
||||||
using google::cloud::speech::v2::RecognitionFeatures_MultiChannelMode_SEPARATE_RECOGNITION_PER_CHANNEL;
|
using google::cloud::speech::v2::RecognitionFeatures_MultiChannelMode_SEPARATE_RECOGNITION_PER_CHANNEL;
|
||||||
using google::cloud::speech::v2::SpeechAdaptation_AdaptationPhraseSet;
|
using google::cloud::speech::v2::SpeechAdaptation_AdaptationPhraseSet;
|
||||||
@@ -158,12 +160,54 @@ GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub>::
|
|||||||
diarization_config->set_max_speaker_count(count);
|
diarization_config->set_max_speaker_count(count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_TRANSCRIPTION_NORMALIZATION")) {
|
||||||
|
// parse JSON string
|
||||||
|
cJSON *json_array = cJSON_Parse(var);
|
||||||
|
|
||||||
|
int array_size = cJSON_GetArraySize(json_array);
|
||||||
|
|
||||||
|
for(int i=0; i<array_size; i++) {
|
||||||
|
cJSON* json_item = cJSON_GetArrayItem(json_array, i);
|
||||||
|
|
||||||
|
auto entry = config->mutable_transcript_normalization()->add_entries();
|
||||||
|
|
||||||
|
std::string search_string = cJSON_GetObjectItem(json_item, "search")->valuestring;
|
||||||
|
std::string replacement_string = cJSON_GetObjectItem(json_item, "replace")->valuestring;
|
||||||
|
bool case_sensitive = cJSON_GetObjectItem(json_item, "case_sensitive")->valueint != 0;
|
||||||
|
|
||||||
|
entry->set_search(search_string);
|
||||||
|
entry->set_replace(replacement_string);
|
||||||
|
entry->set_case_sensitive(case_sensitive);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG,
|
||||||
|
"TRANSCRIPTION_NORMALIZATION search %s, replace %s, set_case_sensitive %d\n", search_string.c_str(), replacement_string.c_str(), case_sensitive);
|
||||||
|
}
|
||||||
|
// clean json
|
||||||
|
cJSON_Delete(json_array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_START_TIMEOUT_MS")) {
|
||||||
|
auto ms = atoi(var);
|
||||||
|
streaming_config->mutable_streaming_features()->mutable_voice_activity_timeout()->mutable_speech_start_timeout()->set_nanos(ms * 1000000);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting speech_start_timeout to %d milliseconds\n", ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_END_TIMEOUT_MS")) {
|
||||||
|
auto ms = atoi(var);
|
||||||
|
streaming_config->mutable_streaming_features()->mutable_voice_activity_timeout()->mutable_speech_end_timeout()->set_nanos(ms * 1000000);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting speech_end_timeout to %d milliseconds\n", ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_ENABLE_VOICE_ACTIVITY_EVENTS")) {
|
||||||
|
bool enabled = !strcmp(var, "true") ? 1 : 0;
|
||||||
|
streaming_config->mutable_streaming_features()->set_enable_voice_activity_events(enabled);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting enable_voice_activity_events to %d \n", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_request.set_recognizer(recognizer);
|
m_request.set_recognizer(recognizer);
|
||||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "using recognizer: %s\n", recognizer.c_str());
|
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "using recognizer: %s\n", recognizer.c_str());
|
||||||
|
|
||||||
// This must be set whether a recognizer id is provided orr not, because it cannot be configured as part of a recognizer.
|
// This must be set whether a recognizer id is provided or not, because it cannot be configured as part of a recognizer.
|
||||||
if (interim > 0) {
|
if (interim > 0) {
|
||||||
streaming_config->mutable_streaming_features()->set_interim_results(interim > 0);
|
streaming_config->mutable_streaming_features()->set_interim_results(interim > 0);
|
||||||
}
|
}
|
||||||
@@ -277,6 +321,12 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
|||||||
streamer->writesDone();
|
streamer->writesDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (speech_event_type == StreamingRecognizeResponse_SpeechEventType_SPEECH_ACTIVITY_BEGIN) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got SPEECH_ACTIVITY_BEGIN\n") ;
|
||||||
|
}
|
||||||
|
else if (speech_event_type == StreamingRecognizeResponse_SpeechEventType_SPEECH_ACTIVITY_END) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got SPEECH_ACTIVITY_END\n") ;
|
||||||
|
}
|
||||||
switch_core_session_rwunlock(session);
|
switch_core_session_rwunlock(session);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got %d responses\n", response.results_size());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got %d responses\n", response.results_size());
|
||||||
}
|
}
|
||||||
@@ -296,9 +346,10 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
|||||||
cb->responseHandler(session, "no_audio", cb->bugname);
|
cb->responseHandler(session, "no_audio", cb->bugname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (status.error_code() != 0) {
|
||||||
cJSON* json = cJSON_CreateObject();
|
cJSON* json = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(json, "type", "error");
|
cJSON_AddStringToObject(json, "type", "error");
|
||||||
|
cJSON_AddStringToObject(json, "error_cause", "stream_close");
|
||||||
cJSON_AddItemToObject(json, "error_code", cJSON_CreateNumber(status.error_code()));
|
cJSON_AddItemToObject(json, "error_code", cJSON_CreateNumber(status.error_code()));
|
||||||
cJSON_AddStringToObject(json, "error_message", status.error_message().c_str());
|
cJSON_AddStringToObject(json, "error_message", status.error_message().c_str());
|
||||||
char* jsonString = cJSON_PrintUnformatted(json);
|
char* jsonString = cJSON_PrintUnformatted(json);
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ static void responseHandler(switch_core_session_t* session, const char * json, c
|
|||||||
switch_channel_event_set_data(channel, event);
|
switch_channel_event_set_data(channel, event);
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "transcription-vendor", "google");
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "transcription-vendor", "google");
|
||||||
}
|
}
|
||||||
|
else if (0 == strcmp("start_of_speech", json)) {
|
||||||
|
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_START_OF_SPEECH);
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "transcription-vendor", "google");
|
||||||
|
}
|
||||||
|
else if (0 == strcmp("end_of_speech", json)) {
|
||||||
|
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_END_OF_SPEECH);
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "transcription-vendor", "google");
|
||||||
|
}
|
||||||
else if (0 == strcmp("end_of_transcript", json)) {
|
else if (0 == strcmp("end_of_transcript", json)) {
|
||||||
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_END_OF_TRANSCRIPT);
|
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_END_OF_TRANSCRIPT);
|
||||||
switch_channel_event_set_data(channel, event);
|
switch_channel_event_set_data(channel, event);
|
||||||
@@ -506,6 +516,14 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_transcribe_load)
|
|||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register subclass %s!\n", TRANSCRIBE_EVENT_END_OF_UTTERANCE);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register subclass %s!\n", TRANSCRIBE_EVENT_END_OF_UTTERANCE);
|
||||||
return SWITCH_STATUS_TERM;
|
return SWITCH_STATUS_TERM;
|
||||||
}
|
}
|
||||||
|
if (switch_event_reserve_subclass(TRANSCRIBE_EVENT_START_OF_SPEECH) != SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register subclass %s!\n", TRANSCRIBE_EVENT_START_OF_SPEECH);
|
||||||
|
return SWITCH_STATUS_TERM;
|
||||||
|
}
|
||||||
|
if (switch_event_reserve_subclass(TRANSCRIBE_EVENT_END_OF_SPEECH) != SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register subclass %s!\n", TRANSCRIBE_EVENT_END_OF_SPEECH);
|
||||||
|
return SWITCH_STATUS_TERM;
|
||||||
|
}
|
||||||
if (switch_event_reserve_subclass(TRANSCRIBE_EVENT_START_OF_TRANSCRIPT) != SWITCH_STATUS_SUCCESS) {
|
if (switch_event_reserve_subclass(TRANSCRIBE_EVENT_START_OF_TRANSCRIPT) != SWITCH_STATUS_SUCCESS) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register subclass %s!\n", TRANSCRIBE_EVENT_START_OF_TRANSCRIPT);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register subclass %s!\n", TRANSCRIBE_EVENT_START_OF_TRANSCRIPT);
|
||||||
return SWITCH_STATUS_TERM;
|
return SWITCH_STATUS_TERM;
|
||||||
@@ -556,6 +574,8 @@ SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_transcribe_shutdown)
|
|||||||
google_speech_cleanup();
|
google_speech_cleanup();
|
||||||
switch_event_free_subclass(TRANSCRIBE_EVENT_RESULTS);
|
switch_event_free_subclass(TRANSCRIBE_EVENT_RESULTS);
|
||||||
switch_event_free_subclass(TRANSCRIBE_EVENT_END_OF_UTTERANCE);
|
switch_event_free_subclass(TRANSCRIBE_EVENT_END_OF_UTTERANCE);
|
||||||
|
switch_event_free_subclass(TRANSCRIBE_EVENT_START_OF_SPEECH);
|
||||||
|
switch_event_free_subclass(TRANSCRIBE_EVENT_END_OF_SPEECH);
|
||||||
switch_event_free_subclass(TRANSCRIBE_EVENT_START_OF_TRANSCRIPT);
|
switch_event_free_subclass(TRANSCRIBE_EVENT_START_OF_TRANSCRIPT);
|
||||||
switch_event_free_subclass(TRANSCRIBE_EVENT_END_OF_TRANSCRIPT);
|
switch_event_free_subclass(TRANSCRIBE_EVENT_END_OF_TRANSCRIPT);
|
||||||
switch_event_free_subclass(TRANSCRIBE_EVENT_NO_AUDIO_DETECTED);
|
switch_event_free_subclass(TRANSCRIBE_EVENT_NO_AUDIO_DETECTED);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#define MY_BUG_NAME "google_transcribe"
|
#define MY_BUG_NAME "google_transcribe"
|
||||||
#define TRANSCRIBE_EVENT_RESULTS "google_transcribe::transcription"
|
#define TRANSCRIBE_EVENT_RESULTS "google_transcribe::transcription"
|
||||||
#define TRANSCRIBE_EVENT_END_OF_UTTERANCE "google_transcribe::end_of_utterance"
|
#define TRANSCRIBE_EVENT_END_OF_UTTERANCE "google_transcribe::end_of_utterance"
|
||||||
|
#define TRANSCRIBE_EVENT_START_OF_SPEECH "google_transcribe::start_of_speech"
|
||||||
|
#define TRANSCRIBE_EVENT_END_OF_SPEECH "google_transcribe::end_of_speech"
|
||||||
#define TRANSCRIBE_EVENT_START_OF_TRANSCRIPT "google_transcribe::start_of_transcript"
|
#define TRANSCRIBE_EVENT_START_OF_TRANSCRIPT "google_transcribe::start_of_transcript"
|
||||||
#define TRANSCRIBE_EVENT_END_OF_TRANSCRIPT "google_transcribe::end_of_transcript"
|
#define TRANSCRIBE_EVENT_END_OF_TRANSCRIPT "google_transcribe::end_of_transcript"
|
||||||
#define TRANSCRIBE_EVENT_NO_AUDIO_DETECTED "google_transcribe::no_audio_detected"
|
#define TRANSCRIBE_EVENT_NO_AUDIO_DETECTED "google_transcribe::no_audio_detected"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Copyright 2023, Drachtio Communications Services, LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
include $(top_srcdir)/build/modmake.rulesam
|
||||||
|
MODNAME=mod_playht_tts
|
||||||
|
|
||||||
|
mod_LTLIBRARIES = mod_playht_tts.la
|
||||||
|
mod_playht_tts_la_SOURCES = mod_playht_tts.c playht_glue.cpp
|
||||||
|
mod_playht_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||||
|
mod_playht_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||||
|
mod_playht_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared -lstdc++ -lboost_system -lboost_thread -lmpg123
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# mod_playht_tts
|
||||||
|
|
||||||
|
A Freeswitch module that allows speak text to speech audio from playht stream.
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#include "mod_playht_tts.h"
|
||||||
|
#include "playht_glue.h"
|
||||||
|
|
||||||
|
SWITCH_MODULE_LOAD_FUNCTION(mod_playht_tts_load);
|
||||||
|
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_playht_tts_shutdown);
|
||||||
|
SWITCH_MODULE_DEFINITION(mod_playht_tts, mod_playht_tts_load, mod_playht_tts_shutdown, NULL);
|
||||||
|
|
||||||
|
static void clearPlayht(playht_t* p, int freeAll) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "clearPlayht\n");
|
||||||
|
if (p->api_key) free(p->api_key);
|
||||||
|
if (p->user_id) free(p->user_id);
|
||||||
|
if (p->quality) free(p->quality);
|
||||||
|
if (p->speed) free(p->speed);
|
||||||
|
if (p->seed) free(p->seed);
|
||||||
|
if (p->temperature) free(p->temperature);
|
||||||
|
if (p->voice_engine) free(p->voice_engine);
|
||||||
|
if (p->emotion) free(p->emotion);
|
||||||
|
if (p->voice_guidance) free(p->voice_guidance);
|
||||||
|
if (p->style_guidance) free(p->style_guidance);
|
||||||
|
if (p->text_guidance) free(p->text_guidance);
|
||||||
|
|
||||||
|
|
||||||
|
if (p->request_id) free(p->request_id);
|
||||||
|
if (p->ct) free(p->ct);
|
||||||
|
if (p->err_msg) free(p->err_msg);
|
||||||
|
if (p->name_lookup_time_ms) free(p->name_lookup_time_ms);
|
||||||
|
if (p->connect_time_ms) free(p->connect_time_ms);
|
||||||
|
if (p->final_response_time_ms) free(p->final_response_time_ms);
|
||||||
|
if (p->cache_filename) free(p->cache_filename);
|
||||||
|
|
||||||
|
|
||||||
|
p->api_key = NULL;
|
||||||
|
p->user_id = NULL;
|
||||||
|
p->quality = NULL;
|
||||||
|
p->speed = NULL;
|
||||||
|
p->seed = NULL;
|
||||||
|
p->temperature = NULL;
|
||||||
|
p->voice_engine = NULL;
|
||||||
|
p->emotion = NULL;
|
||||||
|
p->voice_guidance = NULL;
|
||||||
|
p->style_guidance = NULL;
|
||||||
|
p->text_guidance = NULL;
|
||||||
|
|
||||||
|
p->request_id = NULL;
|
||||||
|
p->ct = NULL;
|
||||||
|
p->err_msg = NULL;
|
||||||
|
p->name_lookup_time_ms = NULL;
|
||||||
|
p->connect_time_ms = NULL;
|
||||||
|
p->final_response_time_ms = NULL;
|
||||||
|
p->cache_filename = NULL;
|
||||||
|
|
||||||
|
if (freeAll) {
|
||||||
|
if (p->voice_name) free(p->voice_name);
|
||||||
|
if (p->session_id) free(p->session_id);
|
||||||
|
p->voice_name = NULL;
|
||||||
|
p->session_id = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static playht_t * createOrRetrievePrivateData(switch_speech_handle_t *sh) {
|
||||||
|
playht_t *p = (playht_t *) sh->private_info;
|
||||||
|
if (!p) {
|
||||||
|
p = switch_core_alloc(sh->memory_pool, sizeof(*p));
|
||||||
|
sh->private_info = p;
|
||||||
|
memset(p, 0, sizeof(*p));
|
||||||
|
switch_mutex_init(&p->mutex, SWITCH_MUTEX_NESTED, sh->memory_pool);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "allocated playht_t\n");
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t p_speech_open(switch_speech_handle_t *sh, const char *voice_name, int rate, int channels, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
playht_t *p = createOrRetrievePrivateData(sh);
|
||||||
|
p->voice_name = strdup(voice_name);
|
||||||
|
p->rate = rate;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "p_speech_open voice: %s, rate %d, channels %d\n", voice_name, rate, channels);
|
||||||
|
return playht_speech_open(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static switch_status_t p_speech_close(switch_speech_handle_t *sh, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
switch_status_t rc;
|
||||||
|
playht_t *p = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "p_speech_close\n");
|
||||||
|
|
||||||
|
switch_mutex_destroy(p->mutex);
|
||||||
|
|
||||||
|
rc = playht_speech_close(p);
|
||||||
|
clearPlayht(p, 1);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeswitch will call this function to feed us text to speak
|
||||||
|
*/
|
||||||
|
static switch_status_t p_speech_feed_tts(switch_speech_handle_t *sh, char *text, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
playht_t *p = createOrRetrievePrivateData(sh);
|
||||||
|
p->draining = 0;
|
||||||
|
p->reads = 0;
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "p_speech_feed_tts\n");
|
||||||
|
|
||||||
|
return playht_speech_feed_tts(p, text, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeswitch calls periodically to get some rendered audio in L16 format. We can provide up to 8k of audio at a time.
|
||||||
|
*/
|
||||||
|
static switch_status_t p_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
playht_t *p = createOrRetrievePrivateData(sh);
|
||||||
|
return playht_speech_read_tts(p, data, datalen, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called at the end, not sure exactly what we need to do here..
|
||||||
|
*/
|
||||||
|
static void p_speech_flush_tts(switch_speech_handle_t *sh)
|
||||||
|
{
|
||||||
|
playht_t *p = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "p_speech_flush_tts\n");
|
||||||
|
playht_speech_flush_tts(p);
|
||||||
|
|
||||||
|
clearPlayht(p, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void p_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
||||||
|
{
|
||||||
|
playht_t *p = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "p_text_param_tts: %s=%s\n", param, val);
|
||||||
|
if (0 == strcmp(param, "api_key")) {
|
||||||
|
if (p->api_key) free(p->api_key);
|
||||||
|
p->api_key = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "user_id")) {
|
||||||
|
if (p->user_id) free(p->user_id);
|
||||||
|
p->user_id = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "quality")) {
|
||||||
|
if (p->quality) free(p->quality);
|
||||||
|
p->quality = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "speed")) {
|
||||||
|
if (p->speed) free(p->speed);
|
||||||
|
p->speed = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "seed")) {
|
||||||
|
if (p->seed) free(p->seed);
|
||||||
|
p->seed = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "temperature")) {
|
||||||
|
if (p->temperature) free(p->temperature);
|
||||||
|
p->temperature = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "voice_engine")) {
|
||||||
|
if (p->voice_engine) free(p->voice_engine);
|
||||||
|
p->voice_engine = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "emotion")) {
|
||||||
|
if (p->emotion) free(p->emotion);
|
||||||
|
p->emotion = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "voice_guidance")) {
|
||||||
|
if (p->voice_guidance) free(p->voice_guidance);
|
||||||
|
p->voice_guidance = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "style_guidance")) {
|
||||||
|
if (p->style_guidance) free(p->style_guidance);
|
||||||
|
p->style_guidance = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "session-uuid")) {
|
||||||
|
if (p->session_id) free(p->session_id);
|
||||||
|
p->session_id = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "write_cache_file") && switch_true(val)) {
|
||||||
|
p->cache_audio = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void p_numeric_param_tts(switch_speech_handle_t *sh, char *param, int val)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
static void p_float_param_tts(switch_speech_handle_t *sh, char *param, double val)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SWITCH_MODULE_LOAD_FUNCTION(mod_playht_tts_load)
|
||||||
|
{
|
||||||
|
switch_speech_interface_t *speech_interface;
|
||||||
|
|
||||||
|
*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 = "playht";
|
||||||
|
speech_interface->speech_open = p_speech_open;
|
||||||
|
speech_interface->speech_close = p_speech_close;
|
||||||
|
speech_interface->speech_feed_tts = p_speech_feed_tts;
|
||||||
|
speech_interface->speech_read_tts = p_speech_read_tts;
|
||||||
|
speech_interface->speech_flush_tts = p_speech_flush_tts;
|
||||||
|
speech_interface->speech_text_param_tts = p_text_param_tts;
|
||||||
|
speech_interface->speech_numeric_param_tts = p_numeric_param_tts;
|
||||||
|
speech_interface->speech_float_param_tts = p_float_param_tts;
|
||||||
|
return playht_speech_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_playht_tts_shutdown)
|
||||||
|
{
|
||||||
|
return playht_speech_unload();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef __MOD_PLAYHT_TTS_H__
|
||||||
|
#define __MOD_PLAYHT_TTS_H__
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
typedef struct playht_data {
|
||||||
|
char *voice_name;
|
||||||
|
char *api_key;
|
||||||
|
char *user_id;
|
||||||
|
char *quality;
|
||||||
|
char *speed;
|
||||||
|
char *seed;
|
||||||
|
char *temperature;
|
||||||
|
char *voice_engine;
|
||||||
|
char *emotion;
|
||||||
|
char *voice_guidance;
|
||||||
|
char *style_guidance;
|
||||||
|
char *text_guidance;
|
||||||
|
|
||||||
|
/* result data */
|
||||||
|
long response_code;
|
||||||
|
char *ct;
|
||||||
|
char *request_id;
|
||||||
|
char *name_lookup_time_ms;
|
||||||
|
char *connect_time_ms;
|
||||||
|
char *final_response_time_ms;
|
||||||
|
char *err_msg;
|
||||||
|
char *cache_filename;
|
||||||
|
char *session_id;
|
||||||
|
|
||||||
|
int rate;
|
||||||
|
int draining;
|
||||||
|
int reads;
|
||||||
|
int cache_audio;
|
||||||
|
|
||||||
|
void *conn;
|
||||||
|
void *circularBuffer;
|
||||||
|
switch_mutex_t *mutex;
|
||||||
|
FILE *file;
|
||||||
|
} playht_t;
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,993 @@
|
|||||||
|
#include "mod_playht_tts.h"
|
||||||
|
#include <switch.h>
|
||||||
|
#include <switch_json.h>
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include <boost/circular_buffer.hpp>
|
||||||
|
#include <boost/thread.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
|
#include <boost/pool/object_pool.hpp>
|
||||||
|
#include <boost/bind/bind.hpp>
|
||||||
|
#include <boost/tokenizer.hpp>
|
||||||
|
#include <boost/foreach.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/assign/list_of.hpp>
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
|
||||||
|
#include "mpg123.h"
|
||||||
|
|
||||||
|
#define BUFFER_GROW_SIZE (8192)
|
||||||
|
#define MP3_DCACHE 8192 * 2
|
||||||
|
|
||||||
|
typedef boost::circular_buffer<uint16_t> CircularBuffer_t;
|
||||||
|
/* Global information, common to all connections */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
CURLM *multi;
|
||||||
|
int still_running;
|
||||||
|
} GlobalInfo_t;
|
||||||
|
static GlobalInfo_t global;
|
||||||
|
|
||||||
|
/* Information associated with a specific easy handle */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
CURL *easy;
|
||||||
|
playht_t* playht;
|
||||||
|
char* body;
|
||||||
|
struct curl_slist *hdr_list;
|
||||||
|
GlobalInfo_t *global;
|
||||||
|
mpg123_handle *mh;
|
||||||
|
char error[CURL_ERROR_SIZE];
|
||||||
|
FILE* file;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> startTime;
|
||||||
|
bool flushed;
|
||||||
|
} ConnInfo_t;
|
||||||
|
|
||||||
|
|
||||||
|
static boost::object_pool<ConnInfo_t> pool ;
|
||||||
|
static std::map<curl_socket_t, boost::asio::ip::tcp::socket *> socket_map;
|
||||||
|
static boost::asio::io_service io_service;
|
||||||
|
static boost::asio::deadline_timer timer(io_service);
|
||||||
|
static std::string fullDirPath;
|
||||||
|
static std::thread worker_thread;
|
||||||
|
|
||||||
|
std::string secondsToMillisecondsString(double seconds) {
|
||||||
|
// Convert to milliseconds
|
||||||
|
double milliseconds = seconds * 1000.0;
|
||||||
|
|
||||||
|
// Truncate to remove fractional part
|
||||||
|
long milliseconds_long = static_cast<long>(milliseconds);
|
||||||
|
|
||||||
|
// Convert to string
|
||||||
|
return std::to_string(milliseconds_long);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURL* createEasyHandle(void) {
|
||||||
|
CURL* easy = curl_easy_init();
|
||||||
|
if(!easy) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "curl_easy_init() failed!\n");
|
||||||
|
return nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_USERAGENT, "jambonz/0.8.5");
|
||||||
|
|
||||||
|
// set connect timeout to 3 seconds and total timeout to 109 seconds
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
|
||||||
|
|
||||||
|
return easy ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cleanupConn(ConnInfo_t *conn) {
|
||||||
|
auto p = conn->playht;
|
||||||
|
|
||||||
|
if (conn->mh) {
|
||||||
|
mpg123_close(conn->mh);
|
||||||
|
mpg123_delete(conn->mh);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( conn->hdr_list ) {
|
||||||
|
curl_slist_free_all(conn->hdr_list);
|
||||||
|
conn->hdr_list = nullptr ;
|
||||||
|
}
|
||||||
|
curl_easy_cleanup(conn->easy);
|
||||||
|
|
||||||
|
if (conn->file) {
|
||||||
|
if (fclose(conn->file) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error closing audio cache file\n");
|
||||||
|
}
|
||||||
|
conn->file = nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
p->conn = nullptr ;
|
||||||
|
p->draining = 1;
|
||||||
|
|
||||||
|
memset(conn, 0, sizeof(ConnInfo_t));
|
||||||
|
pool.destroy(conn) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for completed transfers, and remove their easy handles */
|
||||||
|
void check_multi_info(GlobalInfo_t *g) {
|
||||||
|
CURLMsg *msg;
|
||||||
|
int msgs_left;
|
||||||
|
ConnInfo_t *conn;
|
||||||
|
CURL *easy;
|
||||||
|
CURLcode res;
|
||||||
|
|
||||||
|
while((msg = curl_multi_info_read(g->multi, &msgs_left))) {
|
||||||
|
if(msg->msg == CURLMSG_DONE) {
|
||||||
|
long response_code;
|
||||||
|
double namelookup=0, connect=0, total=0 ;
|
||||||
|
char *ct = NULL ;
|
||||||
|
|
||||||
|
easy = msg->easy_handle;
|
||||||
|
res = msg->data.result;
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &conn);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
|
||||||
|
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_NAMELOOKUP_TIME, &namelookup);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_CONNECT_TIME, &connect);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &total);
|
||||||
|
|
||||||
|
auto p = conn->playht;
|
||||||
|
p->response_code = response_code;
|
||||||
|
if (ct) p->ct = strdup(ct);
|
||||||
|
|
||||||
|
std::string name_lookup_ms = secondsToMillisecondsString(namelookup);
|
||||||
|
std::string connect_ms = secondsToMillisecondsString(connect);
|
||||||
|
std::string final_response_time_ms = secondsToMillisecondsString(total);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
|
||||||
|
"mod_playht_tts: response: %ld, content-type %s,"
|
||||||
|
"dns(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||||
|
"connect(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||||
|
"total(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld\n",
|
||||||
|
response_code, ct,
|
||||||
|
(long)(namelookup), (long)(fmod(namelookup, 1.0) * 1000000),
|
||||||
|
(long)(connect), (long)(fmod(connect, 1.0) * 1000000),
|
||||||
|
(long)(total), (long)(fmod(total, 1.0) * 1000000));
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "name lookup time: %s\n", name_lookup_ms.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "connect time: %s\n", connect_ms.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "final response time: %s\n", final_response_time_ms.c_str());
|
||||||
|
|
||||||
|
p->name_lookup_time_ms = strdup(name_lookup_ms.c_str());
|
||||||
|
p->connect_time_ms = strdup(connect_ms.c_str());
|
||||||
|
p->final_response_time_ms = strdup(final_response_time_ms.c_str());
|
||||||
|
|
||||||
|
curl_multi_remove_handle(g->multi, easy);
|
||||||
|
cleanupConn(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mcode_test(const char *where, CURLMcode code) {
|
||||||
|
if(CURLM_OK != code) {
|
||||||
|
const char *s;
|
||||||
|
switch(code) {
|
||||||
|
case CURLM_CALL_MULTI_PERFORM:
|
||||||
|
s = "CURLM_CALL_MULTI_PERFORM";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_HANDLE:
|
||||||
|
s = "CURLM_BAD_HANDLE";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_EASY_HANDLE:
|
||||||
|
s = "CURLM_BAD_EASY_HANDLE";
|
||||||
|
break;
|
||||||
|
case CURLM_OUT_OF_MEMORY:
|
||||||
|
s = "CURLM_OUT_OF_MEMORY";
|
||||||
|
break;
|
||||||
|
case CURLM_INTERNAL_ERROR:
|
||||||
|
s = "CURLM_INTERNAL_ERROR";
|
||||||
|
break;
|
||||||
|
case CURLM_UNKNOWN_OPTION:
|
||||||
|
s = "CURLM_UNKNOWN_OPTION";
|
||||||
|
break;
|
||||||
|
case CURLM_LAST:
|
||||||
|
s = "CURLM_LAST";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
s = "CURLM_unknown";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_SOCKET:
|
||||||
|
s = "CURLM_BAD_SOCKET";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mcode_test ERROR: %s returns %s:%d\n", where, s, code);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0 ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remsock(int *f, GlobalInfo_t *g) {
|
||||||
|
if(f) {
|
||||||
|
free(f);
|
||||||
|
f = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Called by asio when there is an action on a socket */
|
||||||
|
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp) {
|
||||||
|
int f = *fdp;
|
||||||
|
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X has action %d\n", s, action) ;
|
||||||
|
|
||||||
|
// Socket already POOL REMOVED.
|
||||||
|
if (f == CURL_POLL_REMOVE) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X removed\n", s);
|
||||||
|
remsock(fdp, g);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(socket_map.find(s) == socket_map.end()) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb: socket %#X already closed\n, s");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make sure the event matches what are wanted */
|
||||||
|
if(f == action || f == CURL_POLL_INOUT) {
|
||||||
|
if(error) {
|
||||||
|
action = CURL_CSELECT_ERR;
|
||||||
|
}
|
||||||
|
CURLMcode rc = curl_multi_socket_action(g->multi, s, action, &g->still_running);
|
||||||
|
|
||||||
|
mcode_test("event_cb: curl_multi_socket_action", rc);
|
||||||
|
check_multi_info(g);
|
||||||
|
|
||||||
|
if(g->still_running <= 0) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* keep on watching.
|
||||||
|
* the socket may have been closed and/or fdp may have been changed
|
||||||
|
* in curl_multi_socket_action(), so check them both */
|
||||||
|
if(!error && socket_map.find(s) != socket_map.end() &&
|
||||||
|
(f == action || f == CURL_POLL_INOUT)) {
|
||||||
|
boost::asio::ip::tcp::socket *tcp_socket = socket_map.find(s)->second;
|
||||||
|
|
||||||
|
if(action == CURL_POLL_IN) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
action, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
if(action == CURL_POLL_OUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
action, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* socket functions */
|
||||||
|
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g) {
|
||||||
|
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(s);
|
||||||
|
|
||||||
|
if(it == socket_map.end()) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setsock: socket %#X not found\n, s");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::ip::tcp::socket * tcp_socket = it->second;
|
||||||
|
|
||||||
|
*fdp = act;
|
||||||
|
|
||||||
|
if(act == CURL_POLL_IN) {
|
||||||
|
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(act == CURL_POLL_OUT) {
|
||||||
|
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(act == CURL_POLL_INOUT) {
|
||||||
|
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g) {
|
||||||
|
/* fdp is used to store current action */
|
||||||
|
int *fdp = (int *) calloc(sizeof(int), 1);
|
||||||
|
|
||||||
|
setsock(fdp, s, easy, action, 0, g);
|
||||||
|
curl_multi_assign(g->multi, s, fdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp) {
|
||||||
|
GlobalInfo_t *g = &global;
|
||||||
|
|
||||||
|
int *actionp = (int *) sockp;
|
||||||
|
static const char *whatstr[] = { "none", "IN", "OUT", "INOUT", "REMOVE"};
|
||||||
|
|
||||||
|
if(what == CURL_POLL_REMOVE) {
|
||||||
|
*actionp = what;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(!actionp) {
|
||||||
|
addsock(s, e, what, g);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setsock(actionp, s, e, what, *actionp, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void threadFunc() {
|
||||||
|
/* to make sure the event loop doesn't terminate when there is no work to do */
|
||||||
|
io_service.reset() ;
|
||||||
|
boost::asio::io_service::work work(io_service);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_playht_tts threadFunc - starting\n");
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
io_service.run() ;
|
||||||
|
break ;
|
||||||
|
}
|
||||||
|
catch( std::exception& e) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_playht_tts threadFunc - Error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_playht_tts threadFunc - ending\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Called by asio when our timeout expires */
|
||||||
|
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g)
|
||||||
|
{
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "timer_cb\n");
|
||||||
|
|
||||||
|
if(!error) {
|
||||||
|
CURLMcode rc = curl_multi_socket_action(g->multi, CURL_SOCKET_TIMEOUT, 0, &g->still_running);
|
||||||
|
mcode_test("timer_cb: curl_multi_socket_action", rc);
|
||||||
|
check_multi_info(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g) {
|
||||||
|
|
||||||
|
/* cancel running timer */
|
||||||
|
timer.cancel();
|
||||||
|
|
||||||
|
if(timeout_ms >= 0) {
|
||||||
|
// from libcurl 7.88.1-10+deb12u4 does not allow call curl_multi_socket_action or curl_multi_perform in curl_multi callback directly
|
||||||
|
timer.expires_from_now(boost::posix_time::millisec(timeout_ms ? timeout_ms : 1));
|
||||||
|
timer.async_wait(boost::bind(&timer_cb, boost::placeholders::_1, g));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<uint16_t> convert_mp3_to_linear(ConnInfo_t *conn, uint8_t *data, size_t len) {
|
||||||
|
std::vector<uint16_t> linear_data;
|
||||||
|
int eof = 0;
|
||||||
|
int mp3err = 0;
|
||||||
|
unsigned char decode_buf[MP3_DCACHE];
|
||||||
|
|
||||||
|
if(mpg123_feed(conn->mh, data, len) == MPG123_OK) {
|
||||||
|
while(!eof) {
|
||||||
|
size_t usedlen = 0;
|
||||||
|
off_t frame_offset;
|
||||||
|
unsigned char* audio;
|
||||||
|
|
||||||
|
int decode_status = mpg123_decode_frame(conn->mh, &frame_offset, &audio, &usedlen);
|
||||||
|
|
||||||
|
switch(decode_status) {
|
||||||
|
case MPG123_NEW_FORMAT:
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case MPG123_OK:
|
||||||
|
for(size_t i = 0; i < usedlen; i += 2) {
|
||||||
|
uint16_t value = reinterpret_cast<uint16_t*>(audio)[i / 2];
|
||||||
|
linear_data.push_back(value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MPG123_DONE:
|
||||||
|
case MPG123_NEED_MORE:
|
||||||
|
eof = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MPG123_ERR:
|
||||||
|
default:
|
||||||
|
if(++mp3err >= 5) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Decoder Error!\n");
|
||||||
|
eof = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eof)
|
||||||
|
break;
|
||||||
|
|
||||||
|
mp3err = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return linear_data;
|
||||||
|
}
|
||||||
|
/* CURLOPT_WRITEFUNCTION */
|
||||||
|
static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||||
|
bool fireEvent = false;
|
||||||
|
uint8_t *data = (uint8_t *) ptr;
|
||||||
|
size_t bytes_received = size * nmemb;
|
||||||
|
auto p = conn->playht;
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) p->circularBuffer;
|
||||||
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
|
if (conn->flushed || cBuffer == nullptr) {
|
||||||
|
/* this will abort the transfer */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
switch_mutex_lock(p->mutex);
|
||||||
|
|
||||||
|
if (p->response_code > 0 && p->response_code != 200) {
|
||||||
|
std::string body((char *) ptr, bytes_received);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: received body %s\n", body.c_str());
|
||||||
|
p->err_msg = strdup(body.c_str());
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cache file will stay in the mp3 format for size (smaller) and simplicity */
|
||||||
|
if (conn->file) fwrite(data, sizeof(uint8_t), bytes_received, conn->file);
|
||||||
|
|
||||||
|
pcm_data = convert_mp3_to_linear(conn, data, bytes_received);
|
||||||
|
size_t bytesResampled = pcm_data.size() * sizeof(uint16_t);
|
||||||
|
|
||||||
|
// Resize the buffer if necessary
|
||||||
|
if (cBuffer->capacity() - cBuffer->size() < (bytesResampled / sizeof(uint16_t))) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb growing buffer\n");
|
||||||
|
|
||||||
|
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
|
||||||
|
cBuffer->set_capacity(cBuffer->size() + std::max((bytesResampled / sizeof(uint16_t)), (size_t)BUFFER_GROW_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push the data into the buffer */
|
||||||
|
cBuffer->insert(cBuffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
|
||||||
|
|
||||||
|
if (0 == p->reads++) {
|
||||||
|
fireEvent = true;
|
||||||
|
}
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
}
|
||||||
|
if (fireEvent && p->session_id) {
|
||||||
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - conn->startTime);
|
||||||
|
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||||
|
switch_core_session_t* session = switch_core_session_locate(p->session_id);
|
||||||
|
if (session) {
|
||||||
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
|
if (channel) {
|
||||||
|
switch_event_t *event;
|
||||||
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: firing playback-started\n");
|
||||||
|
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
|
if (p->request_id) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_playht_request_id", p->request_id);
|
||||||
|
}
|
||||||
|
if (p->name_lookup_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_playht_name_lookup_time_ms", p->name_lookup_time_ms);
|
||||||
|
}
|
||||||
|
if (p->connect_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_playht_connect_time_ms", p->connect_time_ms);
|
||||||
|
}
|
||||||
|
if (p->final_response_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_playht_final_response_time_ms", p->final_response_time_ms);
|
||||||
|
}
|
||||||
|
if (p->voice_name) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_playht_voice_name", p->voice_name);
|
||||||
|
}
|
||||||
|
if (p->cache_filename) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", p->cache_filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||||
|
switch_event_fire(&event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", p->session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parseHeader(const std::string& str, std::string& header, std::string& value) {
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
boost::split(parts, str, boost::is_any_of(":"), boost::token_compress_on);
|
||||||
|
|
||||||
|
if (parts.size() != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
header = boost::trim_copy(parts[0]);
|
||||||
|
value = boost::trim_copy(parts[1]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extract_response_code(const std::string& input) {
|
||||||
|
std::size_t space_pos = input.find(' ');
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t code_start_pos = space_pos + 1;
|
||||||
|
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||||
|
if (code_end_pos == std::string::npos) {
|
||||||
|
code_end_pos = input.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||||
|
int response_code = std::stoi(code_str);
|
||||||
|
return response_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||||
|
size_t bytes_received = size * nitems;
|
||||||
|
const std::string prefix = "HTTP/";
|
||||||
|
playht_t* p = conn->playht;
|
||||||
|
std::string header, value;
|
||||||
|
std::string input(buffer, bytes_received);
|
||||||
|
if (parseHeader(input, header, value)) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||||
|
if (0 == header.compare("x-job-ids")) p->request_id = strdup(value.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||||
|
if (input.rfind(prefix, 0) == 0) {
|
||||||
|
try {
|
||||||
|
p->response_code = extract_response_code(input);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", p->response_code);
|
||||||
|
} catch (const std::invalid_argument& e) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_OPENSOCKETFUNCTION */
|
||||||
|
static curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
|
||||||
|
curl_socket_t sockfd = CURL_SOCKET_BAD;
|
||||||
|
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "opensocket: %d\n", purpose);
|
||||||
|
/* restrict to IPv4 */
|
||||||
|
if(purpose == CURLSOCKTYPE_IPCXN && address->family == AF_INET) {
|
||||||
|
/* create a tcp socket object */
|
||||||
|
boost::asio::ip::tcp::socket *tcp_socket = new boost::asio::ip::tcp::socket(io_service);
|
||||||
|
|
||||||
|
/* open it and get the native handle*/
|
||||||
|
boost::system::error_code ec;
|
||||||
|
tcp_socket->open(boost::asio::ip::tcp::v4(), ec);
|
||||||
|
|
||||||
|
if(ec) {
|
||||||
|
/* An error occurred */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't open socket [%ld][%s]\n", ec, ec.message().c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sockfd = tcp_socket->native_handle();
|
||||||
|
|
||||||
|
/* save it for monitoring */
|
||||||
|
socket_map.insert(std::pair<curl_socket_t, boost::asio::ip::tcp::socket *>(sockfd, tcp_socket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_CLOSESOCKETFUNCTION */
|
||||||
|
static int close_socket(void *clientp, curl_socket_t item) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "close_socket : %#X\n", item);
|
||||||
|
|
||||||
|
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(item);
|
||||||
|
if(it != socket_map.end()) {
|
||||||
|
delete it->second;
|
||||||
|
socket_map.erase(it);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
switch_status_t playht_speech_load() {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "playht_speech_loading..\n");
|
||||||
|
memset(&global, 0, sizeof(GlobalInfo_t));
|
||||||
|
global.multi = curl_multi_init();
|
||||||
|
|
||||||
|
if (!global.multi) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "playht_speech_load curl_multi_init() failed, exiting!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_SOCKETFUNCTION, sock_cb);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_SOCKETDATA, &global);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_TIMERDATA, &global);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
||||||
|
|
||||||
|
/* create temp folder for cache files */
|
||||||
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
|
if (!baseDir) {
|
||||||
|
baseDir = "/tmp/";
|
||||||
|
}
|
||||||
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
|
mode_t oldMask = umask(0);
|
||||||
|
int result = mkdir(fullDirPath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
||||||
|
umask(oldMask);
|
||||||
|
if (result != 0) {
|
||||||
|
if (errno != EEXIST) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", fullDirPath.c_str());
|
||||||
|
fullDirPath = "";
|
||||||
|
}
|
||||||
|
else switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "folder %s already exists\n", fullDirPath.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "created folder %s\n", fullDirPath.c_str());
|
||||||
|
}
|
||||||
|
// init mgp123
|
||||||
|
if (mpg123_init() != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to initiate MPG123");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* start worker thread that handles transfers*/
|
||||||
|
std::thread t(threadFunc) ;
|
||||||
|
worker_thread.swap( t ) ;
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "playht_speech_loaded..\n");
|
||||||
|
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_speech_unload() {
|
||||||
|
/* stop the ASIO IO service */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "playht_speech_unload: stopping io service\n");
|
||||||
|
io_service.stop();
|
||||||
|
|
||||||
|
/* Join the worker thread */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "playht_speech_unload: wait for worker thread to complete\n");
|
||||||
|
if (worker_thread.joinable()) {
|
||||||
|
worker_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cleanup curl multi handle*/
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "playht_speech_unload: release curl multi\n");
|
||||||
|
curl_multi_cleanup(global.multi);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "playht_speech_unload: completed\n");
|
||||||
|
|
||||||
|
mpg123_exit();
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_speech_open(playht_t* playht) {
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_speech_feed_tts(playht_t* p, char* text, switch_speech_flag_t *flags) {
|
||||||
|
CURLMcode rc;
|
||||||
|
|
||||||
|
const int MAX_CHARS = 20;
|
||||||
|
char tempText[MAX_CHARS + 4]; // +4 for the ellipsis and null terminator
|
||||||
|
|
||||||
|
if (strlen(text) > MAX_CHARS) {
|
||||||
|
strncpy(tempText, text, MAX_CHARS);
|
||||||
|
strcpy(tempText + MAX_CHARS, "...");
|
||||||
|
} else {
|
||||||
|
strcpy(tempText, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open cache file */
|
||||||
|
if (p->cache_audio && fullDirPath.length() > 0) {
|
||||||
|
switch_uuid_t uuid;
|
||||||
|
char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1];
|
||||||
|
char outfile[512] = "";
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
switch_uuid_get(&uuid);
|
||||||
|
switch_uuid_format(uuid_str, &uuid);
|
||||||
|
|
||||||
|
switch_snprintf(outfile, sizeof(outfile), "%s%s%s.mp3", fullDirPath.c_str(), SWITCH_PATH_SEPARATOR, uuid_str);
|
||||||
|
p->cache_filename = strdup(outfile);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "writing audio cache file to %s\n", p->cache_filename);
|
||||||
|
|
||||||
|
mode_t oldMask = umask(0);
|
||||||
|
fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||||
|
umask(oldMask);
|
||||||
|
if (fd == -1 ) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
p->file = fdopen(fd, "wb");
|
||||||
|
if (!p->file) {
|
||||||
|
close(fd);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p->api_key) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "playht_speech_feed_tts: no api_key provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (!p->user_id) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "playht_speech_feed_tts: no user_id provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* format url*/
|
||||||
|
std::string url = "https://api.play.ht/api/v2/tts/stream";
|
||||||
|
|
||||||
|
/* create the JSON body */
|
||||||
|
cJSON * jResult = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(jResult, "text", text);
|
||||||
|
cJSON_AddStringToObject(jResult, "voice", p->voice_name);
|
||||||
|
cJSON_AddStringToObject(jResult, "output_format", "mp3");
|
||||||
|
cJSON_AddNumberToObject(jResult, "sample_rate", 8000);
|
||||||
|
if (p->voice_engine) {
|
||||||
|
cJSON_AddStringToObject(jResult, "voice_engine", p->voice_engine);
|
||||||
|
}
|
||||||
|
if (p->quality) {
|
||||||
|
cJSON_AddStringToObject(jResult, "quality", p->quality);
|
||||||
|
}
|
||||||
|
if (p->speed) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "speed", atoi(p->speed));
|
||||||
|
}
|
||||||
|
if (p->seed) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "seed", atoi(p->seed));
|
||||||
|
}
|
||||||
|
if (p->temperature) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "temperature", std::strtof(p->temperature, nullptr));
|
||||||
|
}
|
||||||
|
if (p->emotion) {
|
||||||
|
cJSON_AddStringToObject(jResult, "emotion", p->emotion);
|
||||||
|
}
|
||||||
|
if (p->voice_guidance) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "voice_guidance", atoi(p->voice_guidance));
|
||||||
|
}
|
||||||
|
if (p->style_guidance) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "style_guidance", atoi(p->style_guidance));
|
||||||
|
}
|
||||||
|
if (p->text_guidance) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "text_guidance", atoi(p->text_guidance));
|
||||||
|
}
|
||||||
|
char *json = cJSON_PrintUnformatted(jResult);
|
||||||
|
|
||||||
|
cJSON_Delete(jResult);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "playht_speech_feed_tts: [%s] [%s]\n", url.c_str(), tempText);
|
||||||
|
|
||||||
|
ConnInfo_t *conn = pool.malloc() ;
|
||||||
|
|
||||||
|
// COnfigure MPG123
|
||||||
|
int mhError = 0;
|
||||||
|
mpg123_handle *mh = mpg123_new("auto", &mhError);
|
||||||
|
if (!mh) {
|
||||||
|
const char *mhErr = mpg123_plain_strerror(mhError);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error allocating mpg123 handle! %s\n", switch_str_nil(mhErr));
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpg123_open_feed(mh) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpg123_format_all(mh) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpg123_param(mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing single channel!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
CURL* easy = createEasyHandle();
|
||||||
|
p->conn = (void *) conn ;
|
||||||
|
conn->playht = p;
|
||||||
|
conn->easy = easy;
|
||||||
|
conn->mh = mh;
|
||||||
|
conn->global = &global;
|
||||||
|
conn->hdr_list = NULL ;
|
||||||
|
conn->file = p->file;
|
||||||
|
conn->body = json;
|
||||||
|
conn->flushed = false;
|
||||||
|
|
||||||
|
|
||||||
|
p->circularBuffer = (void *) new CircularBuffer_t(8192);
|
||||||
|
|
||||||
|
if (mpg123_param(mh, MPG123_FORCE_RATE, p->rate /*Hz*/, 0) != MPG123_OK) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream api_key_stream;
|
||||||
|
api_key_stream << "AUTHORIZATION: " << p->api_key;
|
||||||
|
std::ostringstream user_id_stream;
|
||||||
|
user_id_stream << "X-USER-ID: " << p->user_id;
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, conn);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, conn->error);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_PRIVATE, conn);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HEADERDATA, conn);
|
||||||
|
|
||||||
|
/* call this function to get a socket */
|
||||||
|
curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, opensocket);
|
||||||
|
|
||||||
|
/* call this function to close a socket */
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CLOSESOCKETFUNCTION, close_socket);
|
||||||
|
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, api_key_stream.str().c_str());
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, user_id_stream.str().c_str());
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, "Accept: audio/mpeg");
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, "Content-Type: application/json");
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, conn->hdr_list);
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, conn->body);
|
||||||
|
//curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, body.length());
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||||
|
|
||||||
|
rc = curl_multi_add_handle(global.multi, conn->easy);
|
||||||
|
mcode_test("new_conn: curl_multi_add_handle", rc);
|
||||||
|
|
||||||
|
/* start a timer to measure the duration until we receive first byte of audio */
|
||||||
|
conn->startTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "playht_speech_feed_tts: called curl_multi_add_handle\n");
|
||||||
|
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_speech_read_tts(playht_t* p, void *data, size_t *datalen, switch_speech_flag_t *flags) {
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) p->circularBuffer;
|
||||||
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
|
{
|
||||||
|
switch_mutex_lock(p->mutex);
|
||||||
|
ConnInfo_t *conn = (ConnInfo_t *) p->conn;
|
||||||
|
if (p->response_code > 0 && p->response_code != 200) {
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "playht_speech_read_tts, returning failure\n") ;
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (conn && conn->flushed) {
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
return SWITCH_STATUS_BREAK;
|
||||||
|
}
|
||||||
|
if (cBuffer->empty()) {
|
||||||
|
if (p->draining) {
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
return SWITCH_STATUS_BREAK;
|
||||||
|
}
|
||||||
|
/* no audio available yet so send silence */
|
||||||
|
memset(data, 255, *datalen);
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
size_t size = std::min((*datalen/2), cBuffer->size());
|
||||||
|
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||||
|
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||||
|
switch_mutex_unlock(p->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(data, pcm_data.data(), pcm_data.size() * sizeof(uint16_t));
|
||||||
|
*datalen = pcm_data.size() * sizeof(uint16_t);
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_speech_flush_tts(playht_t* p) {
|
||||||
|
bool download_complete = p->response_code == 200;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "playht_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
||||||
|
ConnInfo_t *conn = (ConnInfo_t *) p->conn;
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) p->circularBuffer;
|
||||||
|
delete cBuffer;
|
||||||
|
p->circularBuffer = nullptr ;
|
||||||
|
|
||||||
|
if (conn) {
|
||||||
|
conn->flushed = true;
|
||||||
|
if (!download_complete) {
|
||||||
|
if (conn->file) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "closing audio cache file %s because download was interrupted\n", p->cache_filename);
|
||||||
|
if (fclose(conn->file) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "error closing audio cache file\n");
|
||||||
|
}
|
||||||
|
conn->file = nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p->cache_filename) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing audio cache file %s because download was interrupted\n", p->cache_filename);
|
||||||
|
if (unlink(p->cache_filename) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error removing audio cache file %s: %d:%s\n",
|
||||||
|
p->cache_filename, errno, strerror(errno));
|
||||||
|
}
|
||||||
|
free(p->cache_filename);
|
||||||
|
p->cache_filename = nullptr ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p->session_id) {
|
||||||
|
switch_core_session_t* session = switch_core_session_locate(p->session_id);
|
||||||
|
if (session) {
|
||||||
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
|
if (channel) {
|
||||||
|
switch_event_t *event;
|
||||||
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_playht_response_code", std::to_string(p->response_code).c_str());
|
||||||
|
if (p->cache_filename && p->response_code == 200) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", p->cache_filename);
|
||||||
|
}
|
||||||
|
if (p->response_code != 200 && p->err_msg) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_error", p->err_msg);
|
||||||
|
}
|
||||||
|
switch_event_fire(&event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t playht_speech_close(playht_t* p) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "playht_speech_close\n") ;
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef __PLAYHT_GLUE_H__
|
||||||
|
#define __PLAYHT_GLUE_H__
|
||||||
|
|
||||||
|
switch_status_t playht_speech_load();
|
||||||
|
switch_status_t playht_speech_open(playht_t* playht);
|
||||||
|
switch_status_t playht_speech_feed_tts(playht_t* playht, char* text, switch_speech_flag_t *flags);
|
||||||
|
switch_status_t playht_speech_read_tts(playht_t* playht, void *data, size_t *datalen, switch_speech_flag_t *flags);
|
||||||
|
switch_status_t playht_speech_flush_tts(playht_t* playht);
|
||||||
|
switch_status_t playht_speech_close(playht_t* playht);
|
||||||
|
switch_status_t playht_speech_unload();
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Copyright 2023, Drachtio Communications Services, LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
include $(top_srcdir)/build/modmake.rulesam
|
||||||
|
MODNAME=mod_rimelabs_tts
|
||||||
|
|
||||||
|
mod_LTLIBRARIES = mod_rimelabs_tts.la
|
||||||
|
mod_rimelabs_tts_la_SOURCES = mod_rimelabs_tts.c rimelabs_glue.cpp
|
||||||
|
mod_rimelabs_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||||
|
mod_rimelabs_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||||
|
mod_rimelabs_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared -lstdc++ -lboost_system -lboost_thread
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# mod_rimelabs_tts
|
||||||
|
|
||||||
|
A Freeswitch module that allows speak text to speech audio from rimelabs stream.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#include "mod_rimelabs_tts.h"
|
||||||
|
#include "rimelabs_glue.h"
|
||||||
|
|
||||||
|
SWITCH_MODULE_LOAD_FUNCTION(mod_rimelabs_tts_load);
|
||||||
|
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_rimelabs_tts_shutdown);
|
||||||
|
SWITCH_MODULE_DEFINITION(mod_rimelabs_tts, mod_rimelabs_tts_load, mod_rimelabs_tts_shutdown, NULL);
|
||||||
|
|
||||||
|
static void clearrimelabs(rimelabs_t* d, int freeAll) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "clearrimelabs\n");
|
||||||
|
if (d->api_key) free(d->api_key);
|
||||||
|
if (d->model_id) free(d->model_id);
|
||||||
|
if (d->speed_alpha) free(d->speed_alpha);
|
||||||
|
if (d->reduce_latency) free(d->reduce_latency);
|
||||||
|
|
||||||
|
if (d->ct) free(d->ct);
|
||||||
|
if (d->err_msg) free(d->err_msg);
|
||||||
|
if (d->name_lookup_time_ms) free(d->name_lookup_time_ms);
|
||||||
|
if (d->connect_time_ms) free(d->connect_time_ms);
|
||||||
|
if (d->final_response_time_ms) free(d->final_response_time_ms);
|
||||||
|
if (d->cache_filename) free(d->cache_filename);
|
||||||
|
|
||||||
|
|
||||||
|
d->api_key = NULL;
|
||||||
|
d->model_id = NULL;
|
||||||
|
d->speed_alpha = NULL;
|
||||||
|
d->reduce_latency = NULL;
|
||||||
|
|
||||||
|
d->ct = NULL;
|
||||||
|
d->err_msg = NULL;
|
||||||
|
d->name_lookup_time_ms = NULL;
|
||||||
|
d->connect_time_ms = NULL;
|
||||||
|
d->final_response_time_ms = NULL;
|
||||||
|
d->cache_filename = NULL;
|
||||||
|
|
||||||
|
if (freeAll) {
|
||||||
|
if (d->voice_name) free(d->voice_name);
|
||||||
|
if (d->session_id) free(d->session_id);
|
||||||
|
d->voice_name = NULL;
|
||||||
|
d->session_id = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static rimelabs_t * createOrRetrievePrivateData(switch_speech_handle_t *sh) {
|
||||||
|
rimelabs_t *d = (rimelabs_t *) sh->private_info;
|
||||||
|
if (!d) {
|
||||||
|
d = switch_core_alloc(sh->memory_pool, sizeof(*d));
|
||||||
|
sh->private_info = d;
|
||||||
|
memset(d, 0, sizeof(*d));
|
||||||
|
switch_mutex_init(&d->mutex, SWITCH_MUTEX_NESTED, sh->memory_pool);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "allocated rimelabs_t\n");
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t d_speech_open(switch_speech_handle_t *sh, const char *voice_name, int rate, int channels, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
rimelabs_t *d = createOrRetrievePrivateData(sh);
|
||||||
|
d->voice_name = strdup(voice_name);
|
||||||
|
d->rate = rate;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "d_speech_open voice: %s, rate %d, channels %d\n", voice_name, rate, channels);
|
||||||
|
return rimelabs_speech_open(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
static switch_status_t d_speech_close(switch_speech_handle_t *sh, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
switch_status_t rc;
|
||||||
|
rimelabs_t *d = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "d_speech_close\n");
|
||||||
|
|
||||||
|
switch_mutex_destroy(d->mutex);
|
||||||
|
|
||||||
|
rc = rimelabs_speech_close(d);
|
||||||
|
clearrimelabs(d, 1);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeswitch will call this function to feed us text to speak
|
||||||
|
*/
|
||||||
|
static switch_status_t d_speech_feed_tts(switch_speech_handle_t *sh, char *text, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
rimelabs_t *d = createOrRetrievePrivateData(sh);
|
||||||
|
d->draining = 0;
|
||||||
|
d->reads = 0;
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "d_speech_feed_tts\n");
|
||||||
|
|
||||||
|
return rimelabs_speech_feed_tts(d, text, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeswitch calls periodically to get some rendered audio in L16 format. We can provide up to 8k of audio at a time.
|
||||||
|
*/
|
||||||
|
static switch_status_t d_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
||||||
|
{
|
||||||
|
rimelabs_t *d = createOrRetrievePrivateData(sh);
|
||||||
|
return rimelabs_speech_read_tts(d, data, datalen, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called at the end, not sure exactly what we need to do here..
|
||||||
|
*/
|
||||||
|
static void d_speech_flush_tts(switch_speech_handle_t *sh)
|
||||||
|
{
|
||||||
|
rimelabs_t *d = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "d_speech_flush_tts\n");
|
||||||
|
rimelabs_speech_flush_tts(d);
|
||||||
|
|
||||||
|
clearrimelabs(d, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void d_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
||||||
|
{
|
||||||
|
rimelabs_t *d = createOrRetrievePrivateData(sh);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "d_text_param_tts: %s=%s\n", param, val);
|
||||||
|
if (0 == strcmp(param, "api_key")) {
|
||||||
|
if (d->api_key) free(d->api_key);
|
||||||
|
d->api_key = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "model_id")) {
|
||||||
|
if (d->model_id) free(d->model_id);
|
||||||
|
d->model_id = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "speed_alpha")) {
|
||||||
|
if (d->speed_alpha) free(d->speed_alpha);
|
||||||
|
d->speed_alpha = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "reduce_latency")) {
|
||||||
|
if (d->reduce_latency) free(d->reduce_latency);
|
||||||
|
d->reduce_latency = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "voice")) {
|
||||||
|
if (d->voice_name) free(d->voice_name);
|
||||||
|
d->voice_name = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "session-uuid")) {
|
||||||
|
if (d->session_id) free(d->session_id);
|
||||||
|
d->session_id = strdup(val);
|
||||||
|
} else if (0 == strcmp(param, "write_cache_file") && switch_true(val)) {
|
||||||
|
d->cache_audio = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void d_numeric_param_tts(switch_speech_handle_t *sh, char *param, int val)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
static void d_float_param_tts(switch_speech_handle_t *sh, char *param, double val)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SWITCH_MODULE_LOAD_FUNCTION(mod_rimelabs_tts_load)
|
||||||
|
{
|
||||||
|
switch_speech_interface_t *speech_interface;
|
||||||
|
|
||||||
|
*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 = "rimelabs";
|
||||||
|
speech_interface->speech_open = d_speech_open;
|
||||||
|
speech_interface->speech_close = d_speech_close;
|
||||||
|
speech_interface->speech_feed_tts = d_speech_feed_tts;
|
||||||
|
speech_interface->speech_read_tts = d_speech_read_tts;
|
||||||
|
speech_interface->speech_flush_tts = d_speech_flush_tts;
|
||||||
|
speech_interface->speech_text_param_tts = d_text_param_tts;
|
||||||
|
speech_interface->speech_numeric_param_tts = d_numeric_param_tts;
|
||||||
|
speech_interface->speech_float_param_tts = d_float_param_tts;
|
||||||
|
return rimelabs_speech_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_rimelabs_tts_shutdown)
|
||||||
|
{
|
||||||
|
return rimelabs_speech_unload();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#ifndef __MOD_RIMELABS_TTS_H__
|
||||||
|
#define __MOD_RIMELABS_TTS_H__
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include <speex/speex_resampler.h>
|
||||||
|
|
||||||
|
typedef struct rimelabs_data {
|
||||||
|
char *voice_name;
|
||||||
|
char *api_key;
|
||||||
|
char *model_id;
|
||||||
|
char *speed_alpha;
|
||||||
|
char *reduce_latency;
|
||||||
|
|
||||||
|
/* result data */
|
||||||
|
long response_code;
|
||||||
|
char *ct;
|
||||||
|
char *name_lookup_time_ms;
|
||||||
|
char *connect_time_ms;
|
||||||
|
char *final_response_time_ms;
|
||||||
|
char *err_msg;
|
||||||
|
char *cache_filename;
|
||||||
|
char *session_id;
|
||||||
|
|
||||||
|
int rate;
|
||||||
|
int draining;
|
||||||
|
int reads;
|
||||||
|
int cache_audio;
|
||||||
|
|
||||||
|
void *conn;
|
||||||
|
void *circularBuffer;
|
||||||
|
switch_mutex_t *mutex;
|
||||||
|
FILE *file;
|
||||||
|
SpeexResamplerState *resampler;
|
||||||
|
} rimelabs_t;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,950 @@
|
|||||||
|
#include "mod_rimelabs_tts.h"
|
||||||
|
#include <switch.h>
|
||||||
|
#include <switch_json.h>
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <boost/circular_buffer.hpp>
|
||||||
|
#include <boost/thread.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
|
#include <boost/pool/object_pool.hpp>
|
||||||
|
#include <boost/bind/bind.hpp>
|
||||||
|
#include <boost/tokenizer.hpp>
|
||||||
|
#include <boost/foreach.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/assign/list_of.hpp>
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
|
||||||
|
#include <speex/speex_resampler.h>
|
||||||
|
|
||||||
|
#define BUFFER_GROW_SIZE (80000)
|
||||||
|
|
||||||
|
typedef boost::circular_buffer<uint16_t> CircularBuffer_t;
|
||||||
|
/* Global information, common to all connections */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
CURLM *multi;
|
||||||
|
int still_running;
|
||||||
|
} GlobalInfo_t;
|
||||||
|
static GlobalInfo_t global;
|
||||||
|
|
||||||
|
/* Information associated with a specific easy handle */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
CURL *easy;
|
||||||
|
rimelabs_t* rimelabs;
|
||||||
|
char* body;
|
||||||
|
struct curl_slist *hdr_list;
|
||||||
|
GlobalInfo_t *global;
|
||||||
|
char error[CURL_ERROR_SIZE];
|
||||||
|
FILE* file;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> startTime;
|
||||||
|
bool flushed;
|
||||||
|
|
||||||
|
bool has_last_byte;
|
||||||
|
uint8_t last_byte;
|
||||||
|
} ConnInfo_t;
|
||||||
|
|
||||||
|
|
||||||
|
static boost::object_pool<ConnInfo_t> pool ;
|
||||||
|
static std::map<curl_socket_t, boost::asio::ip::tcp::socket *> socket_map;
|
||||||
|
static boost::asio::io_service io_service;
|
||||||
|
static boost::asio::deadline_timer timer(io_service);
|
||||||
|
static std::string fullDirPath;
|
||||||
|
static std::thread worker_thread;
|
||||||
|
|
||||||
|
std::string secondsToMillisecondsString(double seconds) {
|
||||||
|
// Convert to milliseconds
|
||||||
|
double milliseconds = seconds * 1000.0;
|
||||||
|
|
||||||
|
// Truncate to remove fractional part
|
||||||
|
long milliseconds_long = static_cast<long>(milliseconds);
|
||||||
|
|
||||||
|
// Convert to string
|
||||||
|
return std::to_string(milliseconds_long);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURL* createEasyHandle(void) {
|
||||||
|
CURL* easy = curl_easy_init();
|
||||||
|
if(!easy) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "curl_easy_init() failed!\n");
|
||||||
|
return nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_USERAGENT, "jambonz/0.8.5");
|
||||||
|
|
||||||
|
// set connect timeout to 3 seconds and total timeout to 109 seconds
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
|
||||||
|
|
||||||
|
return easy ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cleanupConn(ConnInfo_t *conn) {
|
||||||
|
auto d = conn->rimelabs;
|
||||||
|
|
||||||
|
if( conn->hdr_list ) {
|
||||||
|
curl_slist_free_all(conn->hdr_list);
|
||||||
|
conn->hdr_list = nullptr ;
|
||||||
|
}
|
||||||
|
curl_easy_cleanup(conn->easy);
|
||||||
|
|
||||||
|
if (conn->file) {
|
||||||
|
if (fclose(conn->file) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error closing audio cache file\n");
|
||||||
|
}
|
||||||
|
conn->file = nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->conn = nullptr ;
|
||||||
|
d->draining = 1;
|
||||||
|
|
||||||
|
memset(conn, 0, sizeof(ConnInfo_t));
|
||||||
|
pool.destroy(conn) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for completed transfers, and remove their easy handles */
|
||||||
|
void check_multi_info(GlobalInfo_t *g) {
|
||||||
|
CURLMsg *msg;
|
||||||
|
int msgs_left;
|
||||||
|
ConnInfo_t *conn;
|
||||||
|
CURL *easy;
|
||||||
|
CURLcode res;
|
||||||
|
|
||||||
|
while((msg = curl_multi_info_read(g->multi, &msgs_left))) {
|
||||||
|
if(msg->msg == CURLMSG_DONE) {
|
||||||
|
long response_code;
|
||||||
|
double namelookup=0, connect=0, total=0 ;
|
||||||
|
char *ct = NULL ;
|
||||||
|
|
||||||
|
easy = msg->easy_handle;
|
||||||
|
res = msg->data.result;
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &conn);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
|
||||||
|
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_NAMELOOKUP_TIME, &namelookup);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_CONNECT_TIME, &connect);
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &total);
|
||||||
|
|
||||||
|
auto d = conn->rimelabs;
|
||||||
|
d->response_code = response_code;
|
||||||
|
if (ct) d->ct = strdup(ct);
|
||||||
|
|
||||||
|
std::string name_lookup_ms = secondsToMillisecondsString(namelookup);
|
||||||
|
std::string connect_ms = secondsToMillisecondsString(connect);
|
||||||
|
std::string final_response_time_ms = secondsToMillisecondsString(total);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
|
||||||
|
"mod_rimelabs_tts: response: %ld, content-type %s,"
|
||||||
|
"dns(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||||
|
"connect(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||||
|
"total(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld\n",
|
||||||
|
response_code, ct,
|
||||||
|
(long)(namelookup), (long)(fmod(namelookup, 1.0) * 1000000),
|
||||||
|
(long)(connect), (long)(fmod(connect, 1.0) * 1000000),
|
||||||
|
(long)(total), (long)(fmod(total, 1.0) * 1000000));
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "name lookup time: %s\n", name_lookup_ms.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "connect time: %s\n", connect_ms.c_str());
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "final response time: %s\n", final_response_time_ms.c_str());
|
||||||
|
|
||||||
|
d->name_lookup_time_ms = strdup(name_lookup_ms.c_str());
|
||||||
|
d->connect_time_ms = strdup(connect_ms.c_str());
|
||||||
|
d->final_response_time_ms = strdup(final_response_time_ms.c_str());
|
||||||
|
|
||||||
|
curl_multi_remove_handle(g->multi, easy);
|
||||||
|
cleanupConn(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mcode_test(const char *where, CURLMcode code) {
|
||||||
|
if(CURLM_OK != code) {
|
||||||
|
const char *s;
|
||||||
|
switch(code) {
|
||||||
|
case CURLM_CALL_MULTI_PERFORM:
|
||||||
|
s = "CURLM_CALL_MULTI_PERFORM";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_HANDLE:
|
||||||
|
s = "CURLM_BAD_HANDLE";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_EASY_HANDLE:
|
||||||
|
s = "CURLM_BAD_EASY_HANDLE";
|
||||||
|
break;
|
||||||
|
case CURLM_OUT_OF_MEMORY:
|
||||||
|
s = "CURLM_OUT_OF_MEMORY";
|
||||||
|
break;
|
||||||
|
case CURLM_INTERNAL_ERROR:
|
||||||
|
s = "CURLM_INTERNAL_ERROR";
|
||||||
|
break;
|
||||||
|
case CURLM_UNKNOWN_OPTION:
|
||||||
|
s = "CURLM_UNKNOWN_OPTION";
|
||||||
|
break;
|
||||||
|
case CURLM_LAST:
|
||||||
|
s = "CURLM_LAST";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
s = "CURLM_unknown";
|
||||||
|
break;
|
||||||
|
case CURLM_BAD_SOCKET:
|
||||||
|
s = "CURLM_BAD_SOCKET";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mcode_test ERROR: %s returns %s:%d\n", where, s, code);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0 ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remsock(int *f, GlobalInfo_t *g) {
|
||||||
|
if(f) {
|
||||||
|
free(f);
|
||||||
|
f = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Called by asio when there is an action on a socket */
|
||||||
|
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp) {
|
||||||
|
int f = *fdp;
|
||||||
|
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X has action %d\n", s, action) ;
|
||||||
|
|
||||||
|
// Socket already POOL REMOVED.
|
||||||
|
if (f == CURL_POLL_REMOVE) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X removed\n", s);
|
||||||
|
remsock(fdp, g);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(socket_map.find(s) == socket_map.end()) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb: socket %#X already closed\n, s");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make sure the event matches what are wanted */
|
||||||
|
if(f == action || f == CURL_POLL_INOUT) {
|
||||||
|
if(error) {
|
||||||
|
action = CURL_CSELECT_ERR;
|
||||||
|
}
|
||||||
|
CURLMcode rc = curl_multi_socket_action(g->multi, s, action, &g->still_running);
|
||||||
|
|
||||||
|
mcode_test("event_cb: curl_multi_socket_action", rc);
|
||||||
|
check_multi_info(g);
|
||||||
|
|
||||||
|
if(g->still_running <= 0) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* keep on watching.
|
||||||
|
* the socket may have been closed and/or fdp may have been changed
|
||||||
|
* in curl_multi_socket_action(), so check them both */
|
||||||
|
if(!error && socket_map.find(s) != socket_map.end() &&
|
||||||
|
(f == action || f == CURL_POLL_INOUT)) {
|
||||||
|
boost::asio::ip::tcp::socket *tcp_socket = socket_map.find(s)->second;
|
||||||
|
|
||||||
|
if(action == CURL_POLL_IN) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
action, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
if(action == CURL_POLL_OUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
action, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* socket functions */
|
||||||
|
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g) {
|
||||||
|
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(s);
|
||||||
|
|
||||||
|
if(it == socket_map.end()) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setsock: socket %#X not found\n, s");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::ip::tcp::socket * tcp_socket = it->second;
|
||||||
|
|
||||||
|
*fdp = act;
|
||||||
|
|
||||||
|
if(act == CURL_POLL_IN) {
|
||||||
|
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(act == CURL_POLL_OUT) {
|
||||||
|
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(act == CURL_POLL_INOUT) {
|
||||||
|
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||||
|
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||||
|
boost::bind(&event_cb, g, s,
|
||||||
|
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g) {
|
||||||
|
/* fdp is used to store current action */
|
||||||
|
int *fdp = (int *) calloc(sizeof(int), 1);
|
||||||
|
|
||||||
|
setsock(fdp, s, easy, action, 0, g);
|
||||||
|
curl_multi_assign(g->multi, s, fdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp) {
|
||||||
|
GlobalInfo_t *g = &global;
|
||||||
|
|
||||||
|
int *actionp = (int *) sockp;
|
||||||
|
static const char *whatstr[] = { "none", "IN", "OUT", "INOUT", "REMOVE"};
|
||||||
|
|
||||||
|
if(what == CURL_POLL_REMOVE) {
|
||||||
|
*actionp = what;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(!actionp) {
|
||||||
|
addsock(s, e, what, g);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setsock(actionp, s, e, what, *actionp, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void threadFunc() {
|
||||||
|
/* to make sure the event loop doesn't terminate when there is no work to do */
|
||||||
|
io_service.reset() ;
|
||||||
|
boost::asio::io_service::work work(io_service);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_rimelabs_tts threadFunc - starting\n");
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
io_service.run() ;
|
||||||
|
break ;
|
||||||
|
}
|
||||||
|
catch( std::exception& e) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_rimelabs_tts threadFunc - Error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_rimelabs_tts threadFunc - ending\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Called by asio when our timeout expires */
|
||||||
|
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g)
|
||||||
|
{
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "timer_cb\n");
|
||||||
|
|
||||||
|
if(!error) {
|
||||||
|
CURLMcode rc = curl_multi_socket_action(g->multi, CURL_SOCKET_TIMEOUT, 0, &g->still_running);
|
||||||
|
mcode_test("timer_cb: curl_multi_socket_action", rc);
|
||||||
|
check_multi_info(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g) {
|
||||||
|
|
||||||
|
/* cancel running timer */
|
||||||
|
timer.cancel();
|
||||||
|
|
||||||
|
if(timeout_ms >= 0) {
|
||||||
|
// from libcurl 7.88.1-10+deb12u4 does not allow call curl_multi_socket_action or curl_multi_perform in curl_multi callback directly
|
||||||
|
timer.expires_from_now(boost::posix_time::millisec(timeout_ms ? timeout_ms : 1));
|
||||||
|
timer.async_wait(boost::bind(&timer_cb, boost::placeholders::_1, g));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_WRITEFUNCTION */
|
||||||
|
static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||||
|
bool fireEvent = false;
|
||||||
|
uint8_t *data = (uint8_t *) ptr;
|
||||||
|
size_t bytes_received = size * nmemb;
|
||||||
|
size_t total_bytes_to_process;
|
||||||
|
auto d = conn->rimelabs;
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
||||||
|
|
||||||
|
if (conn->flushed || cBuffer == nullptr) {
|
||||||
|
/* this will abort the transfer */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Buffer to hold combined data if there is unprocessed byte from the last call.
|
||||||
|
std::unique_ptr<uint8_t[]> combinedData;
|
||||||
|
|
||||||
|
if (conn->has_last_byte) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "rimelabs write_cb: processing last byte from previous read\n");
|
||||||
|
|
||||||
|
conn->has_last_byte = false; // We'll handle the last_byte now, so toggle the flag off
|
||||||
|
|
||||||
|
// Allocate memory for the new data array
|
||||||
|
combinedData.reset(new uint8_t[bytes_received + 1]);
|
||||||
|
|
||||||
|
// Prepend the last byte from previous call
|
||||||
|
combinedData[0] = conn->last_byte;
|
||||||
|
|
||||||
|
// Copy the new data following the prepended byte
|
||||||
|
memcpy(combinedData.get() + 1, data, bytes_received);
|
||||||
|
|
||||||
|
// Point our data pointer to the new array
|
||||||
|
data = combinedData.get();
|
||||||
|
|
||||||
|
total_bytes_to_process = bytes_received + 1;
|
||||||
|
} else {
|
||||||
|
total_bytes_to_process = bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we now have an odd total, save the last byte for next time
|
||||||
|
if ((total_bytes_to_process % sizeof(int16_t)) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "rimelabs write_cb: got odd number of bytes %d\n", total_bytes_to_process);
|
||||||
|
|
||||||
|
conn->last_byte = data[total_bytes_to_process - 1];
|
||||||
|
conn->has_last_byte = true;
|
||||||
|
total_bytes_to_process--;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t* inputData = reinterpret_cast<int16_t*>(data);
|
||||||
|
if (0 == d->reads++) {
|
||||||
|
fireEvent = true;
|
||||||
|
// Deepgram return PCM linear16 WAV file which contains 44 bytes headers, remove that.
|
||||||
|
inputData += 22;
|
||||||
|
total_bytes_to_process -= 44;
|
||||||
|
}
|
||||||
|
size_t numSamples = total_bytes_to_process / sizeof(int16_t);
|
||||||
|
{
|
||||||
|
switch_mutex_lock(d->mutex);
|
||||||
|
|
||||||
|
if (d->response_code > 0 && d->response_code != 200) {
|
||||||
|
std::string body((char *) ptr, bytes_received);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: received body %s\n", body.c_str());
|
||||||
|
d->err_msg = strdup(body.c_str());
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cache file will stay in the mp3 format for size (smaller) and simplicity */
|
||||||
|
if (conn->file) fwrite(inputData, sizeof(int16_t), numSamples, conn->file);
|
||||||
|
|
||||||
|
// Resize the buffer if necessary
|
||||||
|
if (cBuffer->capacity() - cBuffer->size() < numSamples) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb growing buffer\n");
|
||||||
|
|
||||||
|
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
|
||||||
|
cBuffer->set_capacity(cBuffer->size() + std::max(numSamples, (size_t)BUFFER_GROW_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push the data into the buffer */
|
||||||
|
cBuffer->insert(cBuffer->end(), inputData, inputData + numSamples);
|
||||||
|
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
}
|
||||||
|
if (fireEvent && d->session_id) {
|
||||||
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - conn->startTime);
|
||||||
|
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||||
|
switch_core_session_t* session = switch_core_session_locate(d->session_id);
|
||||||
|
if (session) {
|
||||||
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
|
if (channel) {
|
||||||
|
switch_event_t *event;
|
||||||
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: firing playback-started\n");
|
||||||
|
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
|
if (d->name_lookup_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_rimelabs_name_lookup_time_ms", d->name_lookup_time_ms);
|
||||||
|
}
|
||||||
|
if (d->connect_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_rimelabs_connect_time_ms", d->connect_time_ms);
|
||||||
|
}
|
||||||
|
if (d->final_response_time_ms) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_rimelabs_final_response_time_ms", d->final_response_time_ms);
|
||||||
|
}
|
||||||
|
if (d->voice_name) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_rimelabs_voice_name", d->voice_name);
|
||||||
|
}
|
||||||
|
if (d->cache_filename) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", d->cache_filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||||
|
switch_event_fire(&event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", d->session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size*nmemb;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parseHeader(const std::string& str, std::string& header, std::string& value) {
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
boost::split(parts, str, boost::is_any_of(":"), boost::token_compress_on);
|
||||||
|
|
||||||
|
if (parts.size() != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
header = boost::trim_copy(parts[0]);
|
||||||
|
value = boost::trim_copy(parts[1]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extract_response_code(const std::string& input) {
|
||||||
|
std::size_t space_pos = input.find(' ');
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t code_start_pos = space_pos + 1;
|
||||||
|
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||||
|
if (code_end_pos == std::string::npos) {
|
||||||
|
code_end_pos = input.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||||
|
int response_code = std::stoi(code_str);
|
||||||
|
return response_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||||
|
size_t bytes_received = size * nitems;
|
||||||
|
const std::string prefix = "HTTP/";
|
||||||
|
rimelabs_t* d = conn->rimelabs;
|
||||||
|
std::string header, value;
|
||||||
|
std::string input(buffer, bytes_received);
|
||||||
|
if (parseHeader(input, header, value)) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||||
|
if (input.rfind(prefix, 0) == 0) {
|
||||||
|
try {
|
||||||
|
d->response_code = extract_response_code(input);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", d->response_code);
|
||||||
|
} catch (const std::invalid_argument& e) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_OPENSOCKETFUNCTION */
|
||||||
|
static curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
|
||||||
|
curl_socket_t sockfd = CURL_SOCKET_BAD;
|
||||||
|
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "opensocket: %d\n", purpose);
|
||||||
|
/* restrict to IPv4 */
|
||||||
|
if(purpose == CURLSOCKTYPE_IPCXN && address->family == AF_INET) {
|
||||||
|
/* create a tcp socket object */
|
||||||
|
boost::asio::ip::tcp::socket *tcp_socket = new boost::asio::ip::tcp::socket(io_service);
|
||||||
|
|
||||||
|
/* open it and get the native handle*/
|
||||||
|
boost::system::error_code ec;
|
||||||
|
tcp_socket->open(boost::asio::ip::tcp::v4(), ec);
|
||||||
|
|
||||||
|
if(ec) {
|
||||||
|
/* An error occurred */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't open socket [%ld][%s]\n", ec, ec.message().c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sockfd = tcp_socket->native_handle();
|
||||||
|
|
||||||
|
/* save it for monitoring */
|
||||||
|
socket_map.insert(std::pair<curl_socket_t, boost::asio::ip::tcp::socket *>(sockfd, tcp_socket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURLOPT_CLOSESOCKETFUNCTION */
|
||||||
|
static int close_socket(void *clientp, curl_socket_t item) {
|
||||||
|
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "close_socket : %#X\n", item);
|
||||||
|
|
||||||
|
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(item);
|
||||||
|
if(it != socket_map.end()) {
|
||||||
|
delete it->second;
|
||||||
|
socket_map.erase(it);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
switch_status_t rimelabs_speech_load() {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "rimelabs_speech_loading..\n");
|
||||||
|
memset(&global, 0, sizeof(GlobalInfo_t));
|
||||||
|
global.multi = curl_multi_init();
|
||||||
|
|
||||||
|
if (!global.multi) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "rimelabs_speech_load curl_multi_init() failed, exiting!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_SOCKETFUNCTION, sock_cb);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_SOCKETDATA, &global);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_TIMERDATA, &global);
|
||||||
|
curl_multi_setopt(global.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
||||||
|
|
||||||
|
/* create temp folder for cache files */
|
||||||
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
|
if (!baseDir) {
|
||||||
|
baseDir = "/tmp/";
|
||||||
|
}
|
||||||
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
|
mode_t oldMask = umask(0);
|
||||||
|
int result = mkdir(fullDirPath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
||||||
|
umask(oldMask);
|
||||||
|
if (result != 0) {
|
||||||
|
if (errno != EEXIST) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", fullDirPath.c_str());
|
||||||
|
fullDirPath = "";
|
||||||
|
}
|
||||||
|
else switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "folder %s already exists\n", fullDirPath.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "created folder %s\n", fullDirPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* start worker thread that handles transfers*/
|
||||||
|
std::thread t(threadFunc) ;
|
||||||
|
worker_thread.swap( t ) ;
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "rimelabs_speech_loaded..\n");
|
||||||
|
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_unload() {
|
||||||
|
/* stop the ASIO IO service */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "rimelabs_speech_unload: stopping io service\n");
|
||||||
|
io_service.stop();
|
||||||
|
|
||||||
|
/* Join the worker thread */
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "rimelabs_speech_unload: wait for worker thread to complete\n");
|
||||||
|
if (worker_thread.joinable()) {
|
||||||
|
worker_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cleanup curl multi handle*/
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "rimelabs_speech_unload: release curl multi\n");
|
||||||
|
curl_multi_cleanup(global.multi);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "rimelabs_speech_unload: completed\n");
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_open(rimelabs_t* rimelabs) {
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_feed_tts(rimelabs_t* d, char* text, switch_speech_flag_t *flags) {
|
||||||
|
CURLMcode rc;
|
||||||
|
|
||||||
|
const int MAX_CHARS = 20;
|
||||||
|
char tempText[MAX_CHARS + 4]; // +4 for the ellipsis and null terminator
|
||||||
|
|
||||||
|
if (strlen(text) > MAX_CHARS) {
|
||||||
|
strncpy(tempText, text, MAX_CHARS);
|
||||||
|
strcpy(tempText + MAX_CHARS, "...");
|
||||||
|
} else {
|
||||||
|
strcpy(tempText, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open cache file */
|
||||||
|
if (d->cache_audio && fullDirPath.length() > 0) {
|
||||||
|
switch_uuid_t uuid;
|
||||||
|
char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1];
|
||||||
|
char outfile[512] = "";
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
switch_uuid_get(&uuid);
|
||||||
|
switch_uuid_format(uuid_str, &uuid);
|
||||||
|
|
||||||
|
switch_snprintf(outfile, sizeof(outfile), "%s%s%s.r8", fullDirPath.c_str(), SWITCH_PATH_SEPARATOR, uuid_str);
|
||||||
|
d->cache_filename = strdup(outfile);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "writing audio cache file to %s\n", d->cache_filename);
|
||||||
|
|
||||||
|
mode_t oldMask = umask(0);
|
||||||
|
fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||||
|
umask(oldMask);
|
||||||
|
if (fd == -1 ) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d->file = fdopen(fd, "wb");
|
||||||
|
if (!d->file) {
|
||||||
|
close(fd);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d->api_key) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "rimelabs_speech_feed_tts: no api_key provided\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
/* format url*/
|
||||||
|
std::string url = "https://users.rime.ai/v1/rime-tts";
|
||||||
|
|
||||||
|
/* create the JSON body */
|
||||||
|
cJSON * jResult = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(jResult, "text", text);
|
||||||
|
cJSON_AddNumberToObject(jResult, "samplingRate", 8000);
|
||||||
|
if (d->voice_name) {
|
||||||
|
cJSON_AddStringToObject(jResult, "speaker", d->voice_name);
|
||||||
|
}
|
||||||
|
if (d->model_id) {
|
||||||
|
cJSON_AddStringToObject(jResult, "modelId", d->model_id);
|
||||||
|
}
|
||||||
|
if (d->speed_alpha) {
|
||||||
|
cJSON_AddNumberToObject(jResult, "speedAlpha", std::strtof(d->speed_alpha, nullptr));
|
||||||
|
}
|
||||||
|
if (d->reduce_latency) {
|
||||||
|
cJSON_AddBoolToObject(jResult, "reduceLatency", !strcmp(d->reduce_latency, "true") ? 1 : 0);
|
||||||
|
}
|
||||||
|
char *json = cJSON_PrintUnformatted(jResult);
|
||||||
|
|
||||||
|
cJSON_Delete(jResult);
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "rimelabs_speech_feed_tts: [%s] [%s]\n", url.c_str(), tempText);
|
||||||
|
|
||||||
|
ConnInfo_t *conn = pool.malloc() ;
|
||||||
|
|
||||||
|
CURL* easy = createEasyHandle();
|
||||||
|
d->conn = (void *) conn ;
|
||||||
|
conn->rimelabs = d;
|
||||||
|
conn->easy = easy;
|
||||||
|
conn->global = &global;
|
||||||
|
conn->hdr_list = NULL ;
|
||||||
|
conn->file = d->file;
|
||||||
|
conn->body = json;
|
||||||
|
conn->flushed = false;
|
||||||
|
conn->has_last_byte = false;
|
||||||
|
conn->last_byte = 0;
|
||||||
|
|
||||||
|
|
||||||
|
d->circularBuffer = (void *) new CircularBuffer_t(BUFFER_GROW_SIZE);
|
||||||
|
// Always use rimelabs at rate 8000 for helping cache audio from jambonz.
|
||||||
|
if (d->rate != 8000) {
|
||||||
|
int err;
|
||||||
|
d->resampler = speex_resampler_init(1, 8000, d->rate, SWITCH_RESAMPLE_QUALITY, &err);
|
||||||
|
if (0 != err) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream api_key_stream;
|
||||||
|
api_key_stream << "Authorization: Bearer " << d->api_key;
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, conn);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, conn->error);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_PRIVATE, conn);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HEADERDATA, conn);
|
||||||
|
|
||||||
|
/* call this function to get a socket */
|
||||||
|
curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, opensocket);
|
||||||
|
|
||||||
|
/* call this function to close a socket */
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CLOSESOCKETFUNCTION, close_socket);
|
||||||
|
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, api_key_stream.str().c_str());
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, "Accept: audio/pcm");
|
||||||
|
conn->hdr_list = curl_slist_append(conn->hdr_list, "Content-Type: application/json");
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, conn->hdr_list);
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, conn->body);
|
||||||
|
//curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, body.length());
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||||
|
|
||||||
|
rc = curl_multi_add_handle(global.multi, conn->easy);
|
||||||
|
mcode_test("new_conn: curl_multi_add_handle", rc);
|
||||||
|
|
||||||
|
/* start a timer to measure the duration until we receive first byte of audio */
|
||||||
|
conn->startTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "rimelabs_speech_feed_tts: called curl_multi_add_handle\n");
|
||||||
|
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_read_tts(rimelabs_t* d, void *data, size_t *datalen, switch_speech_flag_t *flags) {
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
||||||
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
|
{
|
||||||
|
switch_mutex_lock(d->mutex);
|
||||||
|
ConnInfo_t *conn = (ConnInfo_t *) d->conn;
|
||||||
|
if (d->response_code > 0 && d->response_code != 200) {
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "rimelabs_speech_read_tts, returning failure\n") ;
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
if (conn && conn->flushed) {
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
return SWITCH_STATUS_BREAK;
|
||||||
|
}
|
||||||
|
if (cBuffer->empty()) {
|
||||||
|
if (d->draining) {
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
return SWITCH_STATUS_BREAK;
|
||||||
|
}
|
||||||
|
/* no audio available yet so send silence */
|
||||||
|
memset(data, 255, *datalen);
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
size_t size = std::min((*datalen/(2 * d->rate / 8000)), cBuffer->size());
|
||||||
|
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||||
|
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||||
|
switch_mutex_unlock(d->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t data_size = pcm_data.size();
|
||||||
|
|
||||||
|
if (d->resampler) {
|
||||||
|
std::vector<int16_t> in(pcm_data.begin(), pcm_data.end());
|
||||||
|
|
||||||
|
std::vector<int16_t> out((*datalen));
|
||||||
|
spx_uint32_t in_len = data_size;
|
||||||
|
spx_uint32_t out_len = out.size();
|
||||||
|
speex_resampler_process_interleaved_int(d->resampler, in.data(), &in_len, out.data(), &out_len);
|
||||||
|
|
||||||
|
if (out_len > out.size()) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Resampler output exceeded maximum buffer size!\n");
|
||||||
|
return SWITCH_STATUS_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(data, out.data(), out_len * sizeof(int16_t));
|
||||||
|
*datalen = out_len * sizeof(int16_t);
|
||||||
|
} else {
|
||||||
|
memcpy(data, pcm_data.data(), pcm_data.size() * sizeof(uint16_t));
|
||||||
|
*datalen = pcm_data.size() * sizeof(uint16_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_flush_tts(rimelabs_t* d) {
|
||||||
|
bool download_complete = d->response_code == 200;
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "rimelabs_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
||||||
|
|
||||||
|
ConnInfo_t *conn = (ConnInfo_t *) d->conn;
|
||||||
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
||||||
|
delete cBuffer;
|
||||||
|
d->circularBuffer = nullptr ;
|
||||||
|
|
||||||
|
// destroy resampler
|
||||||
|
if (d->resampler) {
|
||||||
|
speex_resampler_destroy(d->resampler);
|
||||||
|
d->resampler = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn) {
|
||||||
|
conn->flushed = true;
|
||||||
|
if (!download_complete) {
|
||||||
|
if (conn->file) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "closing audio cache file %s because download was interrupted\n", d->cache_filename);
|
||||||
|
if (fclose(conn->file) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "error closing audio cache file\n");
|
||||||
|
}
|
||||||
|
conn->file = nullptr ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d->cache_filename) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing audio cache file %s because download was interrupted\n", d->cache_filename);
|
||||||
|
if (unlink(d->cache_filename) != 0) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error removing audio cache file %s: %d:%s\n",
|
||||||
|
d->cache_filename, errno, strerror(errno));
|
||||||
|
}
|
||||||
|
free(d->cache_filename);
|
||||||
|
d->cache_filename = nullptr ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (d->session_id) {
|
||||||
|
switch_core_session_t* session = switch_core_session_locate(d->session_id);
|
||||||
|
if (session) {
|
||||||
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
|
if (channel) {
|
||||||
|
switch_event_t *event;
|
||||||
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||||
|
switch_channel_event_set_data(channel, event);
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_rimelabs_response_code", std::to_string(d->response_code).c_str());
|
||||||
|
if (d->cache_filename && d->response_code == 200) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", d->cache_filename);
|
||||||
|
}
|
||||||
|
if (d->response_code != 200 && d->err_msg) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_error", d->err_msg);
|
||||||
|
}
|
||||||
|
switch_event_fire(&event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_close(rimelabs_t* w) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "rimelabs_speech_close\n") ;
|
||||||
|
return SWITCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef __RIMELABS_GLUE_H__
|
||||||
|
#define __RIMELABS_GLUE_H__
|
||||||
|
|
||||||
|
switch_status_t rimelabs_speech_load();
|
||||||
|
switch_status_t rimelabs_speech_open(rimelabs_t* rimelabs);
|
||||||
|
switch_status_t rimelabs_speech_feed_tts(rimelabs_t* rimelabs, char* text, switch_speech_flag_t *flags);
|
||||||
|
switch_status_t rimelabs_speech_read_tts(rimelabs_t* rimelabs, void *data, size_t *datalen, switch_speech_flag_t *flags);
|
||||||
|
switch_status_t rimelabs_speech_flush_tts(rimelabs_t* rimelabs);
|
||||||
|
switch_status_t rimelabs_speech_close(rimelabs_t* rimelabs);
|
||||||
|
switch_status_t rimelabs_speech_unload();
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -5,4 +5,4 @@ mod_LTLIBRARIES = mod_whisper_tts.la
|
|||||||
mod_whisper_tts_la_SOURCES = mod_whisper_tts.c whisper_glue.cpp
|
mod_whisper_tts_la_SOURCES = mod_whisper_tts.c whisper_glue.cpp
|
||||||
mod_whisper_tts_la_CFLAGS = $(AM_CFLAGS)
|
mod_whisper_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||||
mod_whisper_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
mod_whisper_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||||
mod_whisper_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared `pkg-config --libs boost` -lstdc++ -lmpg123
|
mod_whisper_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared -lstdc++ -lboost_system -lboost_thread -lmpg123
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_whisper_tts_shutdown);
|
|||||||
SWITCH_MODULE_DEFINITION(mod_whisper_tts, mod_whisper_tts_load, mod_whisper_tts_shutdown, NULL);
|
SWITCH_MODULE_DEFINITION(mod_whisper_tts, mod_whisper_tts_load, mod_whisper_tts_shutdown, NULL);
|
||||||
|
|
||||||
static void clearWhisper(whisper_t* w, int freeAll) {
|
static void clearWhisper(whisper_t* w, int freeAll) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "clearWhisper\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "clearWhisper\n");
|
||||||
if (w->api_key) free(w->api_key);
|
if (w->api_key) free(w->api_key);
|
||||||
if (w->model_id) free(w->model_id);
|
if (w->model_id) free(w->model_id);
|
||||||
if (w->speed) free(w->speed);
|
if (w->speed) free(w->speed);
|
||||||
if (w->request_id) free(w->request_id);
|
if (w->request_id) free(w->request_id);
|
||||||
if (w->reported_latency) free(w->reported_latency);
|
if (w->reported_latency) free(w->reported_latency);
|
||||||
|
if (w->reported_organization) free(w->reported_organization);
|
||||||
|
if (w->reported_ratelimit_requests) free(w->reported_ratelimit_requests);
|
||||||
|
if (w->reported_ratelimit_remaining_requests) free(w->reported_ratelimit_remaining_requests);
|
||||||
|
if (w->reported_ratelimit_reset_requests) free(w->reported_ratelimit_reset_requests);
|
||||||
if (w->ct) free(w->ct);
|
if (w->ct) free(w->ct);
|
||||||
if (w->err_msg) free(w->err_msg);
|
if (w->err_msg) free(w->err_msg);
|
||||||
if (w->name_lookup_time_ms) free(w->name_lookup_time_ms);
|
if (w->name_lookup_time_ms) free(w->name_lookup_time_ms);
|
||||||
@@ -25,6 +29,10 @@ static void clearWhisper(whisper_t* w, int freeAll) {
|
|||||||
w->speed = NULL;
|
w->speed = NULL;
|
||||||
w->request_id = NULL;
|
w->request_id = NULL;
|
||||||
w->reported_latency = NULL;
|
w->reported_latency = NULL;
|
||||||
|
w->reported_organization = NULL;
|
||||||
|
w->reported_ratelimit_requests = NULL;
|
||||||
|
w->reported_ratelimit_remaining_requests = NULL;
|
||||||
|
w->reported_ratelimit_reset_requests = NULL;
|
||||||
w->ct = NULL;
|
w->ct = NULL;
|
||||||
w->err_msg = NULL;
|
w->err_msg = NULL;
|
||||||
w->name_lookup_time_ms = NULL;
|
w->name_lookup_time_ms = NULL;
|
||||||
@@ -65,7 +73,7 @@ static switch_status_t w_speech_close(switch_speech_handle_t *sh, switch_speech_
|
|||||||
{
|
{
|
||||||
switch_status_t rc;
|
switch_status_t rc;
|
||||||
whisper_t *w = createOrRetrievePrivateData(sh);
|
whisper_t *w = createOrRetrievePrivateData(sh);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_speech_close\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_close\n");
|
||||||
|
|
||||||
switch_mutex_destroy(w->mutex);
|
switch_mutex_destroy(w->mutex);
|
||||||
|
|
||||||
@@ -83,7 +91,7 @@ static switch_status_t w_speech_feed_tts(switch_speech_handle_t *sh, char *text,
|
|||||||
w->draining = 0;
|
w->draining = 0;
|
||||||
w->reads = 0;
|
w->reads = 0;
|
||||||
|
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_speech_feed_tts\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_feed_tts\n");
|
||||||
|
|
||||||
return whisper_speech_feed_tts(w, text, flags);
|
return whisper_speech_feed_tts(w, text, flags);
|
||||||
}
|
}
|
||||||
@@ -94,7 +102,6 @@ static switch_status_t w_speech_feed_tts(switch_speech_handle_t *sh, char *text,
|
|||||||
static switch_status_t w_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
static switch_status_t w_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
||||||
{
|
{
|
||||||
whisper_t *w = createOrRetrievePrivateData(sh);
|
whisper_t *w = createOrRetrievePrivateData(sh);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_speech_read_tts\n");
|
|
||||||
return whisper_speech_read_tts(w, data, datalen, flags);
|
return whisper_speech_read_tts(w, data, datalen, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +111,7 @@ static switch_status_t w_speech_read_tts(switch_speech_handle_t *sh, void *data,
|
|||||||
static void w_speech_flush_tts(switch_speech_handle_t *sh)
|
static void w_speech_flush_tts(switch_speech_handle_t *sh)
|
||||||
{
|
{
|
||||||
whisper_t *w = createOrRetrievePrivateData(sh);
|
whisper_t *w = createOrRetrievePrivateData(sh);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_speech_flush_tts\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_flush_tts\n");
|
||||||
whisper_speech_flush_tts(w);
|
whisper_speech_flush_tts(w);
|
||||||
|
|
||||||
clearWhisper(w, 0);
|
clearWhisper(w, 0);
|
||||||
@@ -113,7 +120,7 @@ static void w_speech_flush_tts(switch_speech_handle_t *sh)
|
|||||||
static void w_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
static void w_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
||||||
{
|
{
|
||||||
whisper_t *w = createOrRetrievePrivateData(sh);
|
whisper_t *w = createOrRetrievePrivateData(sh);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_text_param_tts: %s=%s\n", param, val);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_text_param_tts: %s=%s\n", param, val);
|
||||||
if (0 == strcmp(param, "api_key")) {
|
if (0 == strcmp(param, "api_key")) {
|
||||||
if (w->api_key) free(w->api_key);
|
if (w->api_key) free(w->api_key);
|
||||||
w->api_key = strdup(val);
|
w->api_key = strdup(val);
|
||||||
|
|||||||
@@ -11,7 +11,18 @@ typedef struct whisper_data {
|
|||||||
/* result data */
|
/* result data */
|
||||||
long response_code;
|
long response_code;
|
||||||
char *ct;
|
char *ct;
|
||||||
|
//whisper headers
|
||||||
|
//openai-organization
|
||||||
|
char *reported_organization;
|
||||||
|
//openai-processing-ms
|
||||||
char *reported_latency;
|
char *reported_latency;
|
||||||
|
//x-ratelimit-limit-requests
|
||||||
|
char *reported_ratelimit_requests;
|
||||||
|
//x-ratelimit-remaining-requests
|
||||||
|
char *reported_ratelimit_remaining_requests;
|
||||||
|
//x-ratelimit-reset-requests
|
||||||
|
char *reported_ratelimit_reset_requests;
|
||||||
|
//x-request-id
|
||||||
char *request_id;
|
char *request_id;
|
||||||
char *name_lookup_time_ms;
|
char *name_lookup_time_ms;
|
||||||
char *connect_time_ms;
|
char *connect_time_ms;
|
||||||
|
|||||||
@@ -436,14 +436,12 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) w->circularBuffer;
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) w->circularBuffer;
|
||||||
std::vector<uint16_t> pcm_data;
|
std::vector<uint16_t> pcm_data;
|
||||||
|
|
||||||
if (conn->flushed) {
|
if (conn->flushed || cBuffer == nullptr) {
|
||||||
/* this will abort the transfer */
|
/* this will abort the transfer */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
switch_mutex_lock(w->mutex);
|
switch_mutex_lock(w->mutex);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: received data, response %ld\n",
|
|
||||||
w->response_code);
|
|
||||||
|
|
||||||
if (w->response_code > 0 && w->response_code != 200) {
|
if (w->response_code > 0 && w->response_code != 200) {
|
||||||
std::string body((char *) ptr, bytes_received);
|
std::string body((char *) ptr, bytes_received);
|
||||||
@@ -482,6 +480,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
switch_core_session_t* session = switch_core_session_locate(w->session_id);
|
switch_core_session_t* session = switch_core_session_locate(w->session_id);
|
||||||
if (session) {
|
if (session) {
|
||||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switch_event_t *event;
|
switch_event_t *event;
|
||||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||||
@@ -496,6 +495,18 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
if (w->request_id) {
|
if (w->request_id) {
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_request_id", w->request_id);
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_request_id", w->request_id);
|
||||||
}
|
}
|
||||||
|
if (w->reported_organization) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_reported_organization", w->reported_latency);
|
||||||
|
}
|
||||||
|
if (w->reported_ratelimit_requests) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_reported_ratelimit_requests", w->reported_ratelimit_requests);
|
||||||
|
}
|
||||||
|
if (w->reported_ratelimit_remaining_requests) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_reported_ratelimit_remaining_requests", w->reported_ratelimit_remaining_requests);
|
||||||
|
}
|
||||||
|
if (w->reported_ratelimit_reset_requests) {
|
||||||
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_reported_ratelimit_reset_requests", w->reported_ratelimit_reset_requests);
|
||||||
|
}
|
||||||
if (w->name_lookup_time_ms) {
|
if (w->name_lookup_time_ms) {
|
||||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_name_lookup_time_ms", w->name_lookup_time_ms);
|
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_whisper_name_lookup_time_ms", w->name_lookup_time_ms);
|
||||||
}
|
}
|
||||||
@@ -525,7 +536,6 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
|||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
}
|
}
|
||||||
switch_core_session_rwunlock(session);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", w->session_id);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", w->session_id);
|
||||||
@@ -546,9 +556,27 @@ static bool parseHeader(const std::string& str, std::string& header, std::string
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int extract_response_code(const std::string& input) {
|
||||||
|
std::size_t space_pos = input.find(' ');
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t code_start_pos = space_pos + 1;
|
||||||
|
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||||
|
if (code_end_pos == std::string::npos) {
|
||||||
|
code_end_pos = input.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||||
|
int response_code = std::stoi(code_str);
|
||||||
|
return response_code;
|
||||||
|
}
|
||||||
|
|
||||||
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||||
size_t bytes_received = size * nitems;
|
size_t bytes_received = size * nitems;
|
||||||
const std::string prefix = "HTTP/2 ";
|
const std::string prefix = "HTTP/";
|
||||||
whisper_t* w = conn->whisper;
|
whisper_t* w = conn->whisper;
|
||||||
std::string header, value;
|
std::string header, value;
|
||||||
std::string input(buffer, bytes_received);
|
std::string input(buffer, bytes_received);
|
||||||
@@ -556,13 +584,17 @@ static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo
|
|||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||||
if (0 == header.compare("openai-processing-ms")) w->reported_latency = strdup(value.c_str());
|
if (0 == header.compare("openai-processing-ms")) w->reported_latency = strdup(value.c_str());
|
||||||
else if (0 == header.compare("x-request-id")) w->request_id = strdup(value.c_str());
|
else if (0 == header.compare("x-request-id")) w->request_id = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("openai-organization")) w->reported_organization = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("x-ratelimit-limit-requests")) w->reported_ratelimit_requests = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("x-ratelimit-remaining-requests")) w->reported_ratelimit_remaining_requests = strdup(value.c_str());
|
||||||
|
else if (0 == header.compare("x-ratelimit-reset-requests")) w->reported_ratelimit_reset_requests = strdup(value.c_str());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s\n", input.c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||||
if (input.rfind(prefix, 0) == 0) {
|
if (input.rfind(prefix, 0) == 0) {
|
||||||
try {
|
try {
|
||||||
w->response_code = std::stoi(input.substr(prefix.length()));
|
w->response_code = extract_response_code(input);
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "parsed response code: %ld\n", w->response_code);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", w->response_code);
|
||||||
} catch (const std::invalid_argument& e) {
|
} catch (const std::invalid_argument& e) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||||
}
|
}
|
||||||
@@ -632,14 +664,14 @@ extern "C" {
|
|||||||
/* create temp folder for cache files */
|
/* create temp folder for cache files */
|
||||||
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||||
if (!baseDir) {
|
if (!baseDir) {
|
||||||
baseDir = "/var/";
|
baseDir = "/tmp/";
|
||||||
}
|
}
|
||||||
if (strcmp(baseDir, "/") == 0) {
|
if (strcmp(baseDir, "/") == 0) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||||
return SWITCH_STATUS_FALSE;
|
return SWITCH_STATUS_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||||
|
|
||||||
// Create the directory with read, write, and execute permissions for everyone
|
// Create the directory with read, write, and execute permissions for everyone
|
||||||
mode_t oldMask = umask(0);
|
mode_t oldMask = umask(0);
|
||||||
@@ -810,6 +842,7 @@ extern "C" {
|
|||||||
switch_codec_implementation_t read_impl;
|
switch_codec_implementation_t read_impl;
|
||||||
switch_core_session_t *psession = switch_core_session_locate(w->session_id);
|
switch_core_session_t *psession = switch_core_session_locate(w->session_id);
|
||||||
switch_core_session_get_read_impl(psession, &read_impl);
|
switch_core_session_get_read_impl(psession, &read_impl);
|
||||||
|
switch_core_session_rwunlock(psession);
|
||||||
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
||||||
if (mpg123_param(mh, MPG123_FORCE_RATE, samples_per_second /*Hz*/, 0) != MPG123_OK) {
|
if (mpg123_param(mh, MPG123_FORCE_RATE, samples_per_second /*Hz*/, 0) != MPG123_OK) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
||||||
@@ -897,8 +930,7 @@ extern "C" {
|
|||||||
|
|
||||||
switch_status_t whisper_speech_flush_tts(whisper_t* w) {
|
switch_status_t whisper_speech_flush_tts(whisper_t* w) {
|
||||||
bool download_complete = w->response_code == 200;
|
bool download_complete = w->response_code == 200;
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "whisper_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "whisper_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
||||||
|
|
||||||
ConnInfo_t *conn = (ConnInfo_t *) w->conn;
|
ConnInfo_t *conn = (ConnInfo_t *) w->conn;
|
||||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) w->circularBuffer;
|
CircularBuffer_t *cBuffer = (CircularBuffer_t *) w->circularBuffer;
|
||||||
delete cBuffer;
|
delete cBuffer;
|
||||||
@@ -930,6 +962,7 @@ extern "C" {
|
|||||||
switch_core_session_t* session = switch_core_session_locate(w->session_id);
|
switch_core_session_t* session = switch_core_session_locate(w->session_id);
|
||||||
if (session) {
|
if (session) {
|
||||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||||
|
switch_core_session_rwunlock(session);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switch_event_t *event;
|
switch_event_t *event;
|
||||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||||
@@ -951,14 +984,13 @@ extern "C" {
|
|||||||
else {
|
else {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||||
}
|
}
|
||||||
switch_core_session_rwunlock(session);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch_status_t whisper_speech_close(whisper_t* w) {
|
switch_status_t whisper_speech_close(whisper_t* w) {
|
||||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "whisper_speech_close\n") ;
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "whisper_speech_close\n") ;
|
||||||
return SWITCH_STATUS_SUCCESS;
|
return SWITCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user