diff --git a/lib/synth-audio.js b/lib/synth-audio.js index fba9dd8..207c5b0 100644 --- a/lib/synth-audio.js +++ b/lib/synth-audio.js @@ -80,7 +80,8 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc logger = logger || noopLogger; assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nvidia', 'elevenlabs', - 'whisper', 'deepgram', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf', 'xai'].includes(vendor) || + 'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf', 'xai'] + .includes(vendor) || vendor.startsWith('custom'), `synthAudio supported vendors are google, aws, microsoft, nvidia and wellsaid ..etc, not ${vendor}`); if ('google' === vendor) { @@ -124,6 +125,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc if (!credentials.deepgram_tts_uri) { assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgram is used'); } + } else if ('deepgramflux' === vendor) { + // Deepgram Flux TTS (/v2/speak); the flux model rides on `voice`/`model` + if (!credentials.deepgram_tts_uri) { + assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgramflux is used'); + } } else if ('xai' === vendor) { assert.ok(credentials.api_key, 'synthAudio requires api_key when xai is used'); } else if ('cartesia' === vendor) { @@ -230,6 +236,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc audioData = await synthDeepgram(logger, {credentials, stats, model, key, text, renderForCaching, disableTtsStreaming, disableTtsCache}); break; + case 'deepgramflux': + audioData = await synthDeepgramFlux(logger, {credentials, stats, model: model || voice, key, text, + renderForCaching, disableTtsStreaming, disableTtsCache}); + break; case 'xai': audioData = await synthXai(logger, { credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, @@ -1140,6 +1150,50 @@ const synthDeepgram = async(logger, {credentials, stats, model, key, text, rende } }; +// Deepgram Flux TTS — the conversation-native model served from /v2/speak. +// Streaming rides the mediajam deepgramflux dialect via a say: filePath; the +// batch/cache path POSTs to /v2/speak (mp3 is batch-only for Flux). +const synthDeepgramFlux = async(logger, {credentials, stats, model, key, text, renderForCaching, + disableTtsStreaming, disableTtsCache}) => { + const {api_key, deepgram_tts_uri} = credentials; + if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) { + let params = '{'; + params += `api_key=${api_key}`; + params += `,playback_id=${key}`; + params += ',vendor=deepgramflux'; + params += `,voice=${model}`; + params += `,write_cache_file=${disableTtsCache ? 0 : 1}`; + if (deepgram_tts_uri) params += `,endpoint=${deepgram_tts_uri}`; + params += '}'; + + return { + filePath: `say:${params}${text.replace(/\n/g, ' ')}`, + servedFromCache: false, + rtt: 0 + }; + } + try { + const post = bent(deepgram_tts_uri || 'https://api.deepgram.com', 'POST', 'buffer', { + // on-premise deepgram does not require to have api_key + ...(api_key && {'Authorization': `Token ${api_key}`}), + 'Accept': 'audio/mpeg', + 'Content-Type': 'application/json' + }); + const audioContent = await post(`/v2/speak?model=${model}`, { + text + }); + return { + audioContent, + extension: 'mp3', + sampleRate: 8000 + }; + } catch (err) { + logger.info({err}, 'synth Deepgram Flux returned error'); + stats.increment('tts.count', ['vendor:deepgramflux', 'accepted:no']); + throw err; + } +}; + const synthXai = async(logger, { credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache }) => { diff --git a/test/synth.js b/test/synth.js index 4d7b660..e34edc9 100644 --- a/test/synth.js +++ b/test/synth.js @@ -1118,6 +1118,34 @@ test('Deepgram speech synth tests', async(t) => { client.quit(); }) +test('Deepgram Flux speech synth tests', async(t) => { + const fn = require('..'); + const {synthAudio, client} = fn(opts, logger); + + if (!process.env.DEEPGRAM_API_KEY) { + t.pass('skipping Deepgram Flux speech synth tests since DEEPGRAM_API_KEY'); + return t.end(); + } + const text = 'Hi there and welcome to jambones!'; + try { + const opts = await synthAudio(stats, { + vendor: 'deepgramflux', + credentials: { + api_key: process.env.DEEPGRAM_API_KEY + }, + model: process.env.DEEPGRAM_FLUX_MODEL || 'flux-alexis-en', + text, + renderForCaching: true + }); + t.ok(!opts.servedFromCache, `successfully synthesized deepgramflux audio to ${opts.filePath}`); + + } catch (err) { + console.error(JSON.stringify(err)); + t.end(err); + } + client.quit(); +}); + test('xai speech synth tests', async(t) => { const fn = require('..'); const {synthAudio, client} = fn(opts, logger);