@jambonz/speech-utils 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/synth-audio.js +75 -1
  2. package/package.json +1 -1
@@ -80,7 +80,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
80
80
  logger = logger || noopLogger;
81
81
 
82
82
  assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nvidia', 'elevenlabs',
83
- 'whisper', 'deepgram', 'rimelabs', 'cartesia', 'inworld', 'resemble'].includes(vendor) ||
83
+ 'whisper', 'deepgram', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf'].includes(vendor) ||
84
84
  vendor.startsWith('custom'),
85
85
  `synthAudio supported vendors are google, aws, microsoft, nvidia and wellsaid ..etc, not ${vendor}`);
86
86
  if ('google' === vendor) {
@@ -127,6 +127,9 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
127
127
  } else if ('cartesia' === vendor) {
128
128
  assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
129
129
  assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
130
+ } else if ('murf' === vendor) {
131
+ assert.ok(voice, 'synthAudio requires voice when murf is used');
132
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when murf is used');
130
133
  } else if (vendor === 'resemble') {
131
134
  assert.ok(voice, 'synthAudio requires voice when resemble is used');
132
135
  assert.ok(credentials.api_key, 'synthAudio requires api_key when resemble is used');
@@ -211,6 +214,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
211
214
  credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
212
215
  disableTtsCache});
213
216
  break;
217
+ case 'murf':
218
+ audioData = await synthMurf(logger, {
219
+ credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
220
+ disableTtsCache});
221
+ break;
214
222
  case 'whisper':
215
223
  audioData = await synthWhisper(logger, {
216
224
  credentials, stats, voice, key, text, instructions, renderForCaching, disableTtsStreaming,
@@ -969,6 +977,72 @@ const synthRimelabs = async(logger, {
969
977
  throw err;
970
978
  }
971
979
  };
980
+
981
+ const synthMurf = async(logger, {
982
+ credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
983
+ }) => {
984
+ const {api_key, model_id, api_uri, options: credOpts} = credentials;
985
+ const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
986
+
987
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
988
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
989
+ /* param keys here must match mod_murf_tts's text_param handler */
990
+ let params = '{';
991
+ params += `api_key=${api_key}`;
992
+ params += `,playback_id=${key}`;
993
+ params += ',vendor=murf';
994
+ params += `,voice=${voice}`;
995
+ if (model_id) params += `,model_id=${model_id}`;
996
+ if (language) params += `,language=${language}`;
997
+ if (api_uri) params += `,api_uri=${api_uri}`;
998
+ if (opts.style) params += `,style=${opts.style}`;
999
+ if (opts.rate !== undefined && opts.rate !== null) params += `,rate=${opts.rate}`;
1000
+ if (opts.pitch !== undefined && opts.pitch !== null) params += `,pitch=${opts.pitch}`;
1001
+ if (opts.variation !== undefined && opts.variation !== null) params += `,variation=${opts.variation}`;
1002
+ params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
1003
+ params += '}';
1004
+
1005
+ return {
1006
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
1007
+ servedFromCache: false,
1008
+ rtt: 0
1009
+ };
1010
+ }
1011
+
1012
+ try {
1013
+ const sampleRate = 8000;
1014
+ /* no Accept header: murf returns 406 if it doesn't match; the response
1015
+ container is selected by the `format` field in the body instead */
1016
+ const post = bent(api_uri || 'https://global.api.murf.ai', 'POST', 'buffer', {
1017
+ 'api-key': api_key,
1018
+ 'Content-Type': 'application/json'
1019
+ });
1020
+ /* murf REST schema is documented loosely; field names follow the SDK params
1021
+ (voice_id/model/format/sample_rate) plus the websocket voice fields. */
1022
+ const audioContent = await post('/v1/speech/stream', {
1023
+ text,
1024
+ voice_id: voice,
1025
+ ...(model_id && {model: model_id}),
1026
+ ...(language && {locale: language}),
1027
+ ...(opts.style && {style: opts.style}),
1028
+ ...(opts.rate !== undefined && opts.rate !== null && {rate: opts.rate}),
1029
+ ...(opts.pitch !== undefined && opts.pitch !== null && {pitch: opts.pitch}),
1030
+ ...(opts.variation !== undefined && opts.variation !== null && {variation: opts.variation}),
1031
+ format: 'WAV',
1032
+ sample_rate: sampleRate,
1033
+ channel_type: 'MONO'
1034
+ });
1035
+ return {
1036
+ audioContent,
1037
+ extension: 'wav',
1038
+ sampleRate
1039
+ };
1040
+ } catch (err) {
1041
+ logger.info({err}, 'synth murf returned error');
1042
+ stats.increment('tts.count', ['vendor:murf', 'accepted:no']);
1043
+ throw err;
1044
+ }
1045
+ };
972
1046
  const synthWhisper = async(logger, {credentials, stats, voice, key, text, instructions,
973
1047
  renderForCaching, disableTtsStreaming, disableTtsCache}) => {
974
1048
  const {api_key, model_id, baseURL, timeout, speed} = credentials;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",