Skip to content
Alfonso² Peterssen edited this page Jul 12, 2017 · 1 revision

Creating a text-to-speech bot

Setup

To create a bot/token follow these instructions. Add telegrambot4s to your build file.

Skeleton

The Google TTS API is very handy, but Google will rightfully block you if abused.

Let's start with the bot skeleton, and add features one by one.

object TextToSpeechBot extends TelegramBot
  with Polling
  with Commands
  with ChatActions
  with InlineQueries {

  // Hardcoded tokens are discouraged.
  // lazy val token = "110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"

  lazy val token = scala.util.Properties
    .envOrNone("BOT_TOKEN")
    .getOrElse(Source.fromFile("bot.token").getLines().mkString)

  def ttsUrl(text: String): String =
    s"http://translate.google.com/translate_tts?client=tw-ob&tl=en-us&q=${URLEncoder.encode(text, "UTF-8")}"
}

/speak command

/speak Education is a progressive discovery of our own ignorance.

Should return an audio transcription of the text.

  onCommand('speak) { implicit msg =>
    // Extracts command arguments (does not includes /command)
    withArgs { args =>
      val text = args.mkString(" ")

      // Using Akka-Http to query the Google TTS API.
      for {
        response <- Http().singleRequest(HttpRequest(uri = Uri(ttsUrl(text))))
        if response.status.isSuccess()
        bytes <- Unmarshal(response).to[ByteString]
      } /* do */ {
        val voiceMp3 = InputFile("voice.mp3", bytes)
        request(SendVoice(msg.source, voiceMp3))
      }
    }
  }

Since uploading an audio file can be time-consuming, let's give the user some feedback:
Just mix ChatActions and use the uploadingAudio to warn the user.

  ...
  uploadingAudio // hint the user about time-consuming operation
  val voiceMp3 = InputFile("voice.mp3", bytes)
  request(SendVoice(msg.source, voiceMp3))
  ...
}

Adding support for listening "previews" is way nicer than blindly download audio files over and over.

@MyAwesomeBot Glory is fleeting, but obscurity is forever.

Should spawn a preview of the audio. Since multiple results can be sent, having a fallback to the /speak command directly from inline mode is also possible.

Mix InlineQueries to have the declarative onInlineQuery method.

Is possible to use onInlineQuery directly but empty queries are problematic.

  onInlineQuery { implicit iq =>
    ...
  }

Note that inline queries must be "answered" even if the query is empty/unhandled.

The "whenOrElse" helper method provides a simple way to filter incoming queries/commands...

  def nonEmptyQuery(iq: InlineQuery): Boolean = iq.query.nonEmpty

  whenOrElse(onInlineQuery, nonEmptyQuery) {
    implicit iq =>
      answerInlineQuery(Seq(

        // Inline "playable" preview
        InlineQueryResultVoice("inline: " + iq.query, ttsUrl(iq.query), iq.query),

        // Redirection to /speak command
        InlineQueryResultArticle("command: " + iq.query, iq.query,
          inputMessageContent = InputTextMessageContent("/speak " + iq.query),
          description = "/speak " + iq.query)))

  } /* empty query */ {
    answerInlineQuery(Seq())(_)
  }

The full source code can be found here: TextToSpeechBot

Clone this wiki locally