@jambonz/speech-utils 0.0.49 → 0.0.51
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/.github/workflows/ci.yml +4 -2
- package/lib/synth-audio.js +126 -4
- package/package.json +1 -1
- package/test/synth.js +75 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -8,8 +8,8 @@ jobs:
|
|
|
8
8
|
build:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
12
|
-
- uses: actions/setup-node@
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
13
|
with:
|
|
14
14
|
node-version: lts/*
|
|
15
15
|
- run: npm install
|
|
@@ -32,5 +32,7 @@ jobs:
|
|
|
32
32
|
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
|
|
33
33
|
ELEVENLABS_VOICE_ID: ${{ secrets.ELEVENLABS_VOICE_ID }}
|
|
34
34
|
ELEVENLABS_MODEL_ID: ${{ secrets.ELEVENLABS_MODEL_ID }}
|
|
35
|
+
PLAYHT_USER_ID: ${{ secrets.PLAYHT_USER_ID }}
|
|
36
|
+
PLAYHT_API_KEY: ${{ secrets.PLAYHT_API_KEY }}
|
|
35
37
|
JAMBONES_HTTP_PROXY_IP: 127.0.0.1
|
|
36
38
|
JAMBONES_HTTP_PROXY_PORT: 3128
|
package/lib/synth-audio.js
CHANGED
|
@@ -84,10 +84,10 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
84
84
|
let rtt;
|
|
85
85
|
logger = logger || noopLogger;
|
|
86
86
|
|
|
87
|
-
assert.ok(['google', 'aws', 'polly', 'microsoft',
|
|
88
|
-
'
|
|
87
|
+
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
|
|
88
|
+
'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
|
|
89
89
|
vendor.startsWith('custom'),
|
|
90
|
-
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
|
|
90
|
+
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
91
91
|
if ('google' === vendor) {
|
|
92
92
|
assert.ok(language, 'synthAudio requires language when google is used');
|
|
93
93
|
}
|
|
@@ -123,6 +123,15 @@ 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');
|
|
131
|
+
} else if ('rimelabs' === vendor) {
|
|
132
|
+
assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
|
|
133
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used');
|
|
134
|
+
assert.ok(credentials.model_id, 'synthAudio requires model_id when rimelabs is used');
|
|
126
135
|
} else if ('whisper' === vendor) {
|
|
127
136
|
assert.ok(voice, 'synthAudio requires voice when whisper is used');
|
|
128
137
|
assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
|
|
@@ -146,7 +155,7 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
146
155
|
) ||
|
|
147
156
|
(
|
|
148
157
|
!process.env.JAMBONES_DISABLE_TTS_STREAMING &&
|
|
149
|
-
['elevenlabs', 'deepgram'].includes(vendor)
|
|
158
|
+
['elevenlabs', 'deepgram', 'rimelabs'].includes(vendor)
|
|
150
159
|
)
|
|
151
160
|
) {
|
|
152
161
|
filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.r8`;
|
|
@@ -206,6 +215,18 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
206
215
|
});
|
|
207
216
|
if (audioBuffer?.filePath) return audioBuffer;
|
|
208
217
|
break;
|
|
218
|
+
case 'playht':
|
|
219
|
+
audioBuffer = await synthPlayHT(logger, {
|
|
220
|
+
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
221
|
+
});
|
|
222
|
+
if (audioBuffer?.filePath) return audioBuffer;
|
|
223
|
+
break;
|
|
224
|
+
case 'rimelabs':
|
|
225
|
+
audioBuffer = await synthRimelabs(logger, {
|
|
226
|
+
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
227
|
+
});
|
|
228
|
+
if (audioBuffer?.filePath) return audioBuffer;
|
|
229
|
+
break;
|
|
209
230
|
case 'whisper':
|
|
210
231
|
audioBuffer = await synthWhisper(logger, {
|
|
211
232
|
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
@@ -682,6 +703,107 @@ const synthElevenlabs = async(logger, {
|
|
|
682
703
|
}
|
|
683
704
|
};
|
|
684
705
|
|
|
706
|
+
const synthPlayHT = async(logger, {
|
|
707
|
+
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
|
|
708
|
+
}) => {
|
|
709
|
+
const {api_key, user_id, voice_engine, options: credOpts} = credentials;
|
|
710
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
711
|
+
|
|
712
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
713
|
+
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
714
|
+
let params = '';
|
|
715
|
+
params += `{api_key=${api_key}`;
|
|
716
|
+
params += `,user_id=${user_id}`;
|
|
717
|
+
params += ',vendor=playht';
|
|
718
|
+
params += `,voice=${voice}`;
|
|
719
|
+
params += `,voice_engine=${voice_engine}`;
|
|
720
|
+
params += ',write_cache_file=1';
|
|
721
|
+
if (opts.quality) params += `,quality=${opts.quality}`;
|
|
722
|
+
if (opts.speed) params += `,speed=${opts.speed}`;
|
|
723
|
+
if (opts.seed) params += `,style=${opts.seed}`;
|
|
724
|
+
if (opts.temperature) params += `,temperature=${opts.temperature}`;
|
|
725
|
+
if (opts.emotion) params += `,emotion=${opts.emotion}`;
|
|
726
|
+
if (opts.voice_guidance) params += `,voice_guidance=${opts.voice_guidance}`;
|
|
727
|
+
if (opts.style_guidance) params += `,style_guidance=${opts.style_guidance}`;
|
|
728
|
+
if (opts.text_guidance) params += `,text_guidance=${opts.text_guidance}`;
|
|
729
|
+
params += '}';
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
733
|
+
servedFromCache: false,
|
|
734
|
+
rtt: 0
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const post = bent('https://api.play.ht', 'POST', 'buffer', {
|
|
740
|
+
'AUTHORIZATION': api_key,
|
|
741
|
+
'X-USER-ID': user_id,
|
|
742
|
+
'Accept': 'audio/mpeg',
|
|
743
|
+
'Content-Type': 'application/json'
|
|
744
|
+
});
|
|
745
|
+
const mp3 = await post('/api/v2/tts/stream', {
|
|
746
|
+
text,
|
|
747
|
+
voice,
|
|
748
|
+
voice_engine,
|
|
749
|
+
output_format: 'mp3',
|
|
750
|
+
sample_rate: 8000,
|
|
751
|
+
...opts
|
|
752
|
+
});
|
|
753
|
+
return mp3;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
logger.info({err}, 'synth PlayHT returned error');
|
|
756
|
+
stats.increment('tts.count', ['vendor:playht', 'accepted:no']);
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const synthRimelabs = async(logger, {
|
|
762
|
+
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
|
|
763
|
+
}) => {
|
|
764
|
+
const {api_key, model_id, options: credOpts} = credentials;
|
|
765
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
766
|
+
|
|
767
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
768
|
+
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
769
|
+
let params = '';
|
|
770
|
+
params += `{api_key=${api_key}`;
|
|
771
|
+
params += `,model_id=${model_id}`;
|
|
772
|
+
params += ',vendor=rimelabs';
|
|
773
|
+
params += `,voice=${voice}`;
|
|
774
|
+
params += ',write_cache_file=1';
|
|
775
|
+
if (opts.speedAlpha) params += `,speed_alpha=${opts.speedAlpha}`;
|
|
776
|
+
if (opts.reduceLatency) params += `,reduce_latency=${opts.reduceLatency}`;
|
|
777
|
+
params += '}';
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
781
|
+
servedFromCache: false,
|
|
782
|
+
rtt: 0
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
const post = bent('https://users.rime.ai', 'POST', 'buffer', {
|
|
788
|
+
'Authorization': `Bearer ${api_key}`,
|
|
789
|
+
'Accept': 'audio/mp3',
|
|
790
|
+
'Content-Type': 'application/json'
|
|
791
|
+
});
|
|
792
|
+
const mp3 = await post('/v1/rime-tts', {
|
|
793
|
+
speaker: voice,
|
|
794
|
+
text,
|
|
795
|
+
modelId: model_id,
|
|
796
|
+
samplingRate: 8000,
|
|
797
|
+
...opts
|
|
798
|
+
});
|
|
799
|
+
return mp3;
|
|
800
|
+
} catch (err) {
|
|
801
|
+
logger.info({err}, 'synth rimelabs returned error');
|
|
802
|
+
stats.increment('tts.count', ['vendor:rimelabs', 'accepted:no']);
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
685
807
|
const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
686
808
|
const {api_key, model_id, baseURL, timeout, speed} = credentials;
|
|
687
809
|
/* 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,81 @@ 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 playht 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
|
+
|
|
593
|
+
test('rimelabs speech synth tests', async(t) => {
|
|
594
|
+
const fn = require('..');
|
|
595
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
596
|
+
|
|
597
|
+
if (!process.env.RIMELABS_API_KEY) {
|
|
598
|
+
t.pass('skipping rimelabs speech synth tests since RIMELABS_API_KEY is not provided');
|
|
599
|
+
return t.end();
|
|
600
|
+
}
|
|
601
|
+
const text = 'Hi there and welcome to jambones!';
|
|
602
|
+
try {
|
|
603
|
+
let opts = await synthAudio(stats, {
|
|
604
|
+
vendor: 'rimelabs',
|
|
605
|
+
credentials: {
|
|
606
|
+
api_key: process.env.RIMELABS_API_KEY,
|
|
607
|
+
model_id: 'mist',
|
|
608
|
+
options: JSON.stringify({
|
|
609
|
+
speedAlpha: 1.0,
|
|
610
|
+
reduceLatency: false
|
|
611
|
+
})
|
|
612
|
+
},
|
|
613
|
+
language: 'en-US',
|
|
614
|
+
voice: 'amber',
|
|
615
|
+
text,
|
|
616
|
+
renderForCaching: true
|
|
617
|
+
});
|
|
618
|
+
t.ok(!opts.servedFromCache, `successfully synthesized rimelabs audio to ${opts.filePath}`);
|
|
619
|
+
|
|
620
|
+
} catch (err) {
|
|
621
|
+
console.error(JSON.stringify(err));
|
|
622
|
+
t.end(err);
|
|
623
|
+
}
|
|
624
|
+
client.quit();
|
|
625
|
+
});
|
|
626
|
+
|
|
552
627
|
test('whisper speech synth tests', async(t) => {
|
|
553
628
|
const fn = require('..');
|
|
554
629
|
const {synthAudio, client} = fn(opts, logger);
|