mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-22 22:17:01 +00:00
feat: add tts driver to puterai module
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"test": "npx mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-polly": "^3.622.0",
|
||||
"@aws-sdk/client-textract": "^3.621.0",
|
||||
"@heyputer/kv.js": "^0.1.3",
|
||||
"@heyputer/multest": "^0.0.2",
|
||||
|
||||
@@ -70,6 +70,34 @@ class AIInterfaceService extends BaseService {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
col_interfaces.set('puter-tts', {
|
||||
description: 'Text-to-speech.',
|
||||
methods: {
|
||||
list_voices: {
|
||||
description: 'List available voices.',
|
||||
parameters: {},
|
||||
},
|
||||
synthesize: {
|
||||
description: 'Synthesize speech from text.',
|
||||
parameters: {
|
||||
text: { type: 'string' },
|
||||
voice: { type: 'string' },
|
||||
language: { type: 'string' },
|
||||
ssml: { type: 'flag' },
|
||||
},
|
||||
result_choices: [
|
||||
{
|
||||
names: ['audio'],
|
||||
type: {
|
||||
$: 'stream',
|
||||
content_type: 'audio',
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
const { PollyClient, SynthesizeSpeechCommand, DescribeVoicesCommand } = require("@aws-sdk/client-polly");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
|
||||
class AWSPollyService extends BaseService {
|
||||
static MODULES = {
|
||||
kv: globalThis.kv,
|
||||
}
|
||||
|
||||
async _construct () {
|
||||
this.clients_ = {};
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-tts']: {
|
||||
async list_voices () {
|
||||
const polly_voices = await this.describe_voices();
|
||||
|
||||
let voices = polly_voices.Voices;
|
||||
|
||||
voices = voices.map((voice) => ({
|
||||
id: voice.Id,
|
||||
name: voice.Name,
|
||||
language: {
|
||||
name: voice.LanguageName,
|
||||
code: voice.LanguageCode,
|
||||
},
|
||||
}))
|
||||
|
||||
return voices;
|
||||
},
|
||||
async synthesize ({
|
||||
text, voice,
|
||||
ssml, language,
|
||||
test_mode,
|
||||
}) {
|
||||
if ( test_mode ) {
|
||||
const url = 'https://puter-sample-data.puter.site/tts_example.mp3'
|
||||
return new TypedValue({
|
||||
$: 'string:url:web',
|
||||
content_type: 'audio',
|
||||
}, url);
|
||||
}
|
||||
|
||||
const polly_speech = await this.synthesize_speech(text, {
|
||||
format: 'mp3',
|
||||
voice_id: voice,
|
||||
text_type: ssml ? 'ssml' : 'text',
|
||||
language,
|
||||
});
|
||||
|
||||
const speech = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'audio/mpeg',
|
||||
}, polly_speech.AudioStream);
|
||||
|
||||
return speech;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_create_aws_credentials () {
|
||||
return {
|
||||
accessKeyId: this.config.aws.access_key,
|
||||
secretAccessKey: this.config.aws.secret_key,
|
||||
};
|
||||
}
|
||||
|
||||
_get_client (region) {
|
||||
if ( ! region ) {
|
||||
region = this.config.aws?.region ?? this.global_config.aws?.region
|
||||
?? 'us-west-2';
|
||||
}
|
||||
if ( this.clients_[region] ) return this.clients_[region];
|
||||
|
||||
this.clients_[region] = new PollyClient({
|
||||
credentials: this._create_aws_credentials(),
|
||||
region,
|
||||
});
|
||||
|
||||
return this.clients_[region];
|
||||
}
|
||||
|
||||
async describe_voices () {
|
||||
let voices = this.modules.kv.get('svc:polly:voices');
|
||||
if ( voices ) {
|
||||
this.log.debug('voices cache hit');
|
||||
return voices;
|
||||
}
|
||||
|
||||
this.log.debug('voices cache miss');
|
||||
|
||||
const client = this._get_client(this.config.aws.region);
|
||||
|
||||
const params = {};
|
||||
|
||||
const command = new DescribeVoicesCommand(params);
|
||||
|
||||
const response = await client.send(command);
|
||||
|
||||
this.modules.kv.set('svc:polly:voices', response);
|
||||
this.modules.kv.expire('svc:polly:voices', 60 * 10); // 10 minutes
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async synthesize_speech (text, { format, voice_id, language, text_type }) {
|
||||
const client = this._get_client(this.config.aws.region);
|
||||
|
||||
let voice = voice_id ?? undefined
|
||||
|
||||
if ( ! voice && language ) {
|
||||
this.log.debug('getting language appropriate voice', { language });
|
||||
voice = await this.maybe_get_language_appropriate_voice_(language);
|
||||
}
|
||||
|
||||
if ( ! voice ) {
|
||||
voice = 'Salli';
|
||||
}
|
||||
|
||||
this.log.debug('using voice', { voice });
|
||||
|
||||
const params = {
|
||||
OutputFormat: format,
|
||||
Text: text,
|
||||
VoiceId: voice,
|
||||
LanguageCode: language ?? 'en-US',
|
||||
TextType: text_type ?? 'text',
|
||||
};
|
||||
|
||||
const command = new SynthesizeSpeechCommand(params);
|
||||
|
||||
const response = await client.send(command);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async maybe_get_language_appropriate_voice_ (language) {
|
||||
const voices = await this.describe_voices();
|
||||
|
||||
const voice = voices.Voices.find((voice) => {
|
||||
return voice.LanguageCode === language;
|
||||
});
|
||||
|
||||
if ( ! voice ) return null;
|
||||
|
||||
return voice.Id;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AWSPollyService,
|
||||
};
|
||||
@@ -10,6 +10,9 @@ class PuterAIModule extends AdvancedBase {
|
||||
const { AWSTextractService } = require('./AWSTextractService');
|
||||
services.registerService('aws-textract', AWSTextractService);
|
||||
|
||||
const { AWSPollyService } = require('./AWSPollyService');
|
||||
services.registerService('aws-polly', AWSPollyService);
|
||||
|
||||
const { OpenAICompletionService } = require('./OpenAICompletionService');
|
||||
services.registerService('openai-completion', OpenAICompletionService);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user