Text-to-speech.
authorCMDR furrycat <elite@furrycat.net>
Mon, 23 Oct 2017 09:17:12 +0000 (10:17 +0100)
committerCMDR furrycat <elite@furrycat.net>
Mon, 23 Oct 2017 09:17:12 +0000 (10:17 +0100)
bot.py
plugin/play/play.py
requirements.txt

diff --git a/bot.py b/bot.py
index a9300d5..7d740ac 100644 (file)
--- 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!")
index 7fa8e4e..d64c57f 100644 (file)
@@ -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)
index 1cef42a..8bc055f 100644 (file)
@@ -6,4 +6,5 @@ pexpect
 pillow
 pyzmq
 pytz
+speake3
 youtube_dl