From: CMDR furrycat Date: Mon, 23 Oct 2017 09:17:12 +0000 (+0100) Subject: Text-to-speech. X-Git-Url: http://git.furryclan.net/?a=commitdiff_plain;h=246182b66735a447abc53fe764e433e2b7068ece;p=furrycat%2Fcatbot.git Text-to-speech. --- diff --git a/bot.py b/bot.py index a9300d5..7d740ac 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,9 @@ import os import pytz import random import re +import speake3 import sys +import tempfile import time import traceback import urllib.request @@ -608,6 +610,36 @@ async def play_sound(channel, filename, *, join = True): result = await maybe_play_sound(channel, filename, probability = 1.0, join = join) return result +async def tts(channel, text, args = {}): + try: + fd, wav = tempfile.mkstemp('.tts') + os.close(fd) + except: + log.exception('mkstemp') + return False + try: + engine = speake3.Speake() + params = { + 'pitch': '97', + 'speed': '250', + 'voice': 'mb-en1' + } + params.update(args) + for k, v in params.items(): + log.debug('{} {}'.format(k, v)) + engine.set(k, v) + engine.set('wavfile', wav) + engine.say(text) + engine.talkback() + except: + log.exception('speake3') + os.unlink(wav) + return False + + await play_sound(channel, wav) + os.unlink(wav) + return True + def lower_role_than(member, *, strict = False): if member.server.me is None: log.error("Can't call lower_role_than() a member on a server we aren't on!") diff --git a/plugin/play/play.py b/plugin/play/play.py index 7fa8e4e..d64c57f 100644 --- a/plugin/play/play.py +++ b/plugin/play/play.py @@ -14,19 +14,33 @@ import cat class Play(object): def description(self): - return 'Play audio files.' + return 'Play audio files and text-to-speech.' def valid_commands(self): - return ['play'] + return ['play', 'tts'] async def handle_command(self, message, command, raw): if command not in self.valid_commands(): return PluginCommand.ignored - await self.play(message, shlex.split(raw)[1:]) + if command == 'play': + await self.play(message, shlex.split(raw)[1:]) + elif command == 'tts': + parts = raw.split(':') + args = shlex.split(parts[0])[1:] + if len(parts) > 1: + text = ':'.join(parts[1:]).strip() + await self.play(message, args, text) + elif args[0] == 'help': + await self.help_tts(message) + else: + await bot.say(message.channel, 'what?') return PluginCommand.exclusive async def handle_help(self, message, command, *args): - await self.help_play(message) + if command == 'play': + await self.help_play(message) + elif command == 'tts': + await self.help_tts(message) async def help_play(self, message): lines = [ @@ -41,17 +55,43 @@ class Play(object): ] await bot.say_many(message.channel, lines) - async def play(self, message, args): + async def help_tts(self, message): + lines = [ + 'Text-to-speech:', + '```', + 'tts in CHANNEL [ARGS]: MESSAGE', + 'tts to MEMBER [ARGS]: MESSAGE', + '', + 'If you do not specify a #channel I will play to the channel you are in.', + "If you specify `to` a user I will play to that user's channel instead." + '```', + 'The `ARGS` to `tts` are one or more of:', + '', + '`voice VOICE`', + '', + '`amplitude 0-200` default `100', + '', + '`pitch 0-99` default `97`', + '', + '`speed 80-450` default `250`' + ] + await bot.say_many(message.channel, lines) + + async def play(self, message, args, text = None): if not len(args): await bot.maybe_say(message.channel, 'what?', probability = 0.5) return False if args[0].lower() == 'help': - await self.help_play(message) + if text is None: + await self.help_play(message) + else: + await self.help_tts(message) return True url = None voice = None + tts = {} i = 0 while i < len(args): @@ -87,7 +127,11 @@ class Play(object): log.info('Parsed user {} is not in any shared voice channel'.format(user)) if voice is not None: ok = True - else: + elif arg in ['amplitude', 'pitch', 'speed', 'voice']: + if param is not None: + tts[arg] = param + ok = True + elif not text: # arg is lowercase! url = args[i] i -= 1 @@ -101,7 +145,7 @@ class Play(object): if ok and not voice: voice = bot.voice_channel_for_user(message.author) - if not ok or not url or not voice: + if not ok or not (url or text) or not voice: log.info('Failed to parse play command. Got url {} channel {}'.format(url, voice)) if not url: await bot.say(message.channel, 'what?') @@ -112,11 +156,18 @@ class Play(object): server = voice.server member = server.get_member(message.author.id) if bot.is_admin(member, server) or member.top_role >= server.me.top_role: - log.info('Sound {} requested by {} in {} on {}'.format(url, message.author, voice.name, server.name)) await bot.wake_up() - result = await bot.play_sound(voice, url) + if text: + log.info('Text-to-speech request by {} in {} on {}: {}'.format(message.author, voice.name, server.name, text)) + result = await bot.tts(voice, text, tts) + else: + log.info('Sound {} requested by {} in {} on {}'.format(url, message.author, voice.name, server.name)) + result = await bot.play_sound(voice, url) if not result: await cat.yelp(message.channel) else: - log.info('User {} is not authorised to request sound {} in {} on {}'.format(message.author, url, voice.name, server.name)) + if text: + log.info('User {} is not authorised to request text-to-speech in {} on {}'.format(message.author, voice.name, server.name)) + else: + log.info('User {} is not authorised to request sound {} in {} on {}'.format(message.author, url, voice.name, server.name)) await cat.hiss(message.channel) diff --git a/requirements.txt b/requirements.txt index 1cef42a..8bc055f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ pexpect pillow pyzmq pytz +speake3 youtube_dl