@jambonz/speech-utils 0.0.49 → 0.0.50
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.
- package/lib/synth-audio.js +67 -1
- package/package.json +1 -1
- package/test/synth.js +41 -0
package/lib/synth-audio.js
CHANGED
|
@@ -85,7 +85,7 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
85
85
|
logger = logger || noopLogger;
|
|
86
86
|
|
|
87
87
|
assert.ok(['google', 'aws', 'polly', 'microsoft',
|
|
88
|
-
'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper', 'deepgram'].includes(vendor) ||
|
|
88
|
+
'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper', 'deepgram', 'playht'].includes(vendor) ||
|
|
89
89
|
vendor.startsWith('custom'),
|
|
90
90
|
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
|
|
91
91
|
if ('google' === vendor) {
|
|
@@ -123,6 +123,11 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
123
123
|
assert.ok(voice, 'synthAudio requires voice when elevenlabs is used');
|
|
124
124
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when elevenlabs is used');
|
|
125
125
|
assert.ok(credentials.model_id, 'synthAudio requires model_id when elevenlabs is used');
|
|
126
|
+
} else if ('playht' === vendor) {
|
|
127
|
+
assert.ok(voice, 'synthAudio requires voice when playht is used');
|
|
128
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when playht is used');
|
|
129
|
+
assert.ok(credentials.user_id, 'synthAudio requires user_id when playht is used');
|
|
130
|
+
assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine when playht is used');
|
|
126
131
|
} else if ('whisper' === vendor) {
|
|
127
132
|
assert.ok(voice, 'synthAudio requires voice when whisper is used');
|
|
128
133
|
assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
|
|
@@ -206,6 +211,12 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
206
211
|
});
|
|
207
212
|
if (audioBuffer?.filePath) return audioBuffer;
|
|
208
213
|
break;
|
|
214
|
+
case 'playht':
|
|
215
|
+
audioBuffer = await synthPlayHT(logger, {
|
|
216
|
+
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
217
|
+
});
|
|
218
|
+
if (audioBuffer?.filePath) return audioBuffer;
|
|
219
|
+
break;
|
|
209
220
|
case 'whisper':
|
|
210
221
|
audioBuffer = await synthWhisper(logger, {
|
|
211
222
|
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
@@ -682,6 +693,61 @@ const synthElevenlabs = async(logger, {
|
|
|
682
693
|
}
|
|
683
694
|
};
|
|
684
695
|
|
|
696
|
+
const synthPlayHT = async(logger, {
|
|
697
|
+
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
|
|
698
|
+
}) => {
|
|
699
|
+
const {api_key, user_id, voice_engine, options: credOpts} = credentials;
|
|
700
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
701
|
+
|
|
702
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
703
|
+
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
704
|
+
let params = '';
|
|
705
|
+
params += `{api_key=${api_key}`;
|
|
706
|
+
params += `,user_id=${user_id}`;
|
|
707
|
+
params += ',vendor=playht';
|
|
708
|
+
params += `,voice=${voice}`;
|
|
709
|
+
params += `,voice_engine=${voice_engine}`;
|
|
710
|
+
params += ',write_cache_file=1';
|
|
711
|
+
if (opts.quality) params += `,quality=${opts.quality}`;
|
|
712
|
+
if (opts.speed) params += `,speed=${opts.speed}`;
|
|
713
|
+
if (opts.seed) params += `,style=${opts.seed}`;
|
|
714
|
+
if (opts.temperature) params += `,temperature=${opts.temperature}`;
|
|
715
|
+
if (opts.emotion) params += `,emotion=${opts.emotion}`;
|
|
716
|
+
if (opts.voice_guidance) params += `,voice_guidance=${opts.voice_guidance}`;
|
|
717
|
+
if (opts.style_guidance) params += `,style_guidance=${opts.style_guidance}`;
|
|
718
|
+
if (opts.text_guidance) params += `,text_guidance=${opts.text_guidance}`;
|
|
719
|
+
params += '}';
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
723
|
+
servedFromCache: false,
|
|
724
|
+
rtt: 0
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
const post = bent('https://api.play.ht', 'POST', 'buffer', {
|
|
730
|
+
'AUTHORIZATION': api_key,
|
|
731
|
+
'X-USER-ID': user_id,
|
|
732
|
+
'Accept': 'audio/mpeg',
|
|
733
|
+
'Content-Type': 'application/json'
|
|
734
|
+
});
|
|
735
|
+
const mp3 = await post('/api/v2/tts/stream', {
|
|
736
|
+
text,
|
|
737
|
+
voice,
|
|
738
|
+
voice_engine,
|
|
739
|
+
output_format: 'mp3',
|
|
740
|
+
sample_rate: 8000,
|
|
741
|
+
...opts
|
|
742
|
+
});
|
|
743
|
+
return mp3;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
logger.info({err}, 'synth PlayHT returned error');
|
|
746
|
+
stats.increment('tts.count', ['vendor:playht', 'accepted:no']);
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
685
751
|
const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
686
752
|
const {api_key, model_id, baseURL, timeout, speed} = credentials;
|
|
687
753
|
/* if the env is set to stream then bag out, unless we are specifically rendering to generate a cache file */
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -549,6 +549,47 @@ test('Elevenlabs speech synth tests', async(t) => {
|
|
|
549
549
|
client.quit();
|
|
550
550
|
})
|
|
551
551
|
|
|
552
|
+
test('PlayHT speech synth tests', async(t) => {
|
|
553
|
+
const fn = require('..');
|
|
554
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
555
|
+
|
|
556
|
+
if (!process.env.PLAYHT_API_KEY || !process.env.PLAYHT_USER_ID) {
|
|
557
|
+
t.pass('skipping PlayHT speech synth tests since PLAYHT_API_KEY or PLAYHT_USER_ID is/are not provided');
|
|
558
|
+
return t.end();
|
|
559
|
+
}
|
|
560
|
+
const text = 'Hi there and welcome to jambones!';
|
|
561
|
+
try {
|
|
562
|
+
let opts = await synthAudio(stats, {
|
|
563
|
+
vendor: 'playht',
|
|
564
|
+
credentials: {
|
|
565
|
+
api_key: process.env.PLAYHT_API_KEY,
|
|
566
|
+
user_id: process.env.PLAYHT_USER_ID,
|
|
567
|
+
voice_engine: 'PlayHT2.0-turbo',
|
|
568
|
+
options: JSON.stringify({
|
|
569
|
+
quality: "medium",
|
|
570
|
+
speed: 1,
|
|
571
|
+
seed: 1,
|
|
572
|
+
temperature: 1,
|
|
573
|
+
emotion: "female_happy",
|
|
574
|
+
voice_guidance: 3,
|
|
575
|
+
style_guidance: 20,
|
|
576
|
+
text_guidance: 1,
|
|
577
|
+
})
|
|
578
|
+
},
|
|
579
|
+
language: 'en-US',
|
|
580
|
+
voice: 's3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/female-cs/manifest.json',
|
|
581
|
+
text,
|
|
582
|
+
renderForCaching: true
|
|
583
|
+
});
|
|
584
|
+
t.ok(!opts.servedFromCache, `successfully synthesized eleven audio to ${opts.filePath}`);
|
|
585
|
+
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error(JSON.stringify(err));
|
|
588
|
+
t.end(err);
|
|
589
|
+
}
|
|
590
|
+
client.quit();
|
|
591
|
+
})
|
|
592
|
+
|
|
552
593
|
test('whisper speech synth tests', async(t) => {
|
|
553
594
|
const fn = require('..');
|
|
554
595
|
const {synthAudio, client} = fn(opts, logger);
|