SwearPi - Using Amazon Polly to read swearing git commits

SwearPi - Using Amazon Polly to read swearing git commits

Recently I stumbled upon a twitter account called Developers Swearing @gitlost, created by by @uiri00. The bio of the account sums up what it does better than I ever could “Unfiltered commit messages containing profanity from GitHub's API”. With every tweet I read I could feel compassion towards the developer making that commit. For some odd reason I thought I’d enable mobile notifications for the account. There is something about getting a notification, looking down at my watch and seeing “fixed a whole lot of shit” and just feeling some connection with that random developer.

But do you know what would be better is if rather than text, this was audiobably played in my home office. And that is where the idea for the SwearPi came from. Project seems pretty simple and I’ve broke its tasks down into a few steps.

  1. Get some twitter API to read @gitlost
  2. Use Amazon Polly to convert that tweet to text
  3. Feel shame, for all the great things I could contribute to the world… I made this instead.

I know I want to use php for this rather than banging my head on the table working with bash scripts or python. It's simple enough and it’ll get the job done quickly. First things first though, install php and some additional packages I will need.

sudo apt-get install php7.0-cli php7.0-curl php7.0-simplexml

We'll need to install composer because it will be required to install Amazon SDK for php.

While we’re installing things we may as well also install mpg321 as we’ll need it to play mp3 files.

sudo apt-get install mpg321

Twitter API

Simple enough to do. Go to apps.twitter.com, create new app, create access token via the dashboard. Grab a simple library to speed up the process. I found twitter-api-php which appears to do exactly what I want. So to install it with composer

php composer.phar require j7mbo/twitter-api-php

And and from their examples this came together in about 15 seconds flat. Copying oauth_access_token, oauth_access_token_secret, consumer_key and consumer_secret from our twitter app page that we created before, finding the right API endpoint to read a users timeline, then throwing all those together gives this php script.

<?php

    require_once 'vendor/autoload.php';

    $settings = array(
        'oauth_access_token' => 'XXXX',
        'oauth_access_token_secret' => 'XXXX',
        'consumer_key' => 'XXXX',
        'consumer_secret' => 'XXXX'
    );
    
    $url = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
    $getfield = '?screen_name=gitlost&count=2';
    $requestMethod = 'GET';
    $twitter = new TwitterAPIExchange($settings);

    $twitter_json = json_decode($twitter->setGetfield($getfield)
        ->buildOauth($url, $requestMethod)
        ->performRequest(), true);
    
    var_dump($twitter_json);

Now to test it in the terminal and if everything has gone to plan it'll output the 2 most recent tweets. I won't be going through the extra twitter code I am going to add but it will all be attached at the bottom and minimally commented. It's just a few extra tweaks, caching tweets, marking them as spoken, etc.

Amazon Polly

Having a raspberry Pi swear at me may get repetitive, so I'm going to use the DescribeVoices voices method to query a list of voices. Then when its time to read out a tweet, use a random voice. I'll cache this like I did with tweets and then reload it once a week. As I want more than just the Australian voices (there is only two...) I will also be listing all of the Great Britain and United States voices.

I'm not going to go into too much detail about what I am doing with Polly as its pretty simple and full source code is provided at the bottom of this post. In short, its using synthesizeSpeech to take some text to convert to an mp3 stream, which we write to a temp file with file_put_contents and then read out loud with mpg321.

All the code up until now is functional, all there is left to do is add it to a cronjob and now I have a fully functional developer powered swearing raspberry pi.

Full source code:

<?php
    require_once 'vendor/autoload.php';
    
    // Setup twitter and parse recent tweets.
    $settings = array(
        'oauth_access_token' => 'XXXX',
        'oauth_access_token_secret' => 'XXXX',
        'consumer_key' => 'XXXX',
        'consumer_secret' => 'XXXX'
    );

    $url = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
    $getfield = '?screen_name=gitlost&count=20';
    $requestMethod = 'GET';
    $twitter = new TwitterAPIExchange($settings);

    $twitter_json = json_decode($twitter->setGetfield($getfield)
        ->buildOauth($url, $requestMethod)
        ->performRequest(), true);
    
    $recent_tweets = array();
    
    // If we have a tweets.json file, lets load it.
    if (file_exists('tweets.json'))
    {
        $recent_tweets = json_decode(file_get_contents('tweets.json'), true);
    }

    // Loop through each tweet, adding new tweets, but only save if the file changed.
    // We want to keep a record of tweets that have been spoken, as we dont want this to keep saying tweets its already said.
    $did_change_file = false;
    foreach ($twitter_json as $tweet)
    {
        if (isset($recent_tweets[$tweet['id_str']]) == false)
        {
            $did_change_file = true;
            $recent_tweets[$tweet['id_str']]['text'] = $tweet['text'];
            $recent_tweets[$tweet['id_str']]['spoken'] = false;
            $recent_tweets[$tweet['id_str']]['created_at'] = strtotime($tweet['created_at']);
        }
    }
    if ($did_change_file)
    {
        file_put_contents('tweets.json', json_encode($recent_tweets));
    }
   
    // Now lets setup Amazon Polly. 
    // I have no credentials for AWS here as I have them setup in  ~/.aws/
    // See how here, https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
    $pollyClient = new \Aws\Polly\PollyClient([
        'version'     => '2016-06-10',
        'region'      => 'us-east-1',
    ]);

    // A function we'll use to describe voices of a given language code.
    function describeVoices($languageCode)
    {
        global $pollyClient;
        
        $return = array();

        $result = $pollyClient->describeVoices([
            'LanguageCode' => $languageCode,
        ]);
    
        foreach ($result['Voices'] as $voice)
        {
            $return[] = $voice['Id'];
        }

        return $return;
    }

    // A function to load multiple language codes and store it in a json file as a cache.
    function updateVoices()
    {
        $voice_store = array();
        $voice_store['created'] = time();
        $voice_store['voices'] = array();
        $voice_store['voices'] = array_merge($voice_store['voices'], describeVoices('en-AU'));
        $voice_store['voices'] = array_merge($voice_store['voices'], describeVoices('en-GB'));
        $voice_store['voices'] = array_merge($voice_store['voices'], describeVoices('en-US'));

        file_put_contents('voices.json', json_encode($voice_store));

        return $voice_store['voices'];
    }
   
    // Decide what we want to do, load the existing voices.json, updated it, or create it.
    if (file_exists('voices.json'))
    {
        $voice_store = json_decode(file_get_contents('voices.json'), true);
        if (time() - (60 * 60 * 24 * 7) > $voice_store['created'])
        {
            $voices = updateVoices();
        }
        else
        {
            $voices = $voice_store['voices'];
        }
    }
    else
    {
        $voices = updateVoices();
    }

    // Now lets find a tweet that has not been spoken yet.
    $tweet_text = null;

    foreach ($recent_tweets as &$tweet)
    {
        if ($tweet['spoken'] == false)
        {
            $tweet_text = $tweet['text'];
            $tweet['spoken'] = true;
            break;
        }
    }
    // Pro-tip: This is important, as $tweet is a refernce variable (&) and we could mess things up when we use the same variable name below.
    unset($tweet);

    // If we found a weet, lets fucking send it.
    if ($tweet_text != null)
    {
        // Seeing as we will save the file, lets trim old tweets.
        $old_recent_tweets = $recent_tweets;
        $recent_tweets = array();
        $one_hour_ago = time() - (60 * 60);
        foreach ($old_recent_tweets as $tweet_id => $tweet)
        {
            if ($one_hour_ago < $tweet['created_at'])
            {
                $recent_tweets[$tweet_id] = $tweet;
            }
        }
        file_put_contents('tweets.json', json_encode($recent_tweets));

        // Now that we have a tweet, lets get the mp3 from Amazon Polly.
        // One thing to note is the '. ' I put in front of the text. This is becuase I am
        // playing through a bluetooth speaker. Not having this (at least for me) will cause
        // the first word or two to be lost.
        echo "Saying $tweet_text";   
        $result = $pollyClient->synthesizeSpeech([
            'OutputFormat' => 'mp3',
            'Text' => '. '.$tweet_text,
            'VoiceId' => $voices[array_rand($voices)],
        ]);

        // Now we have a stream, lets download it to a file, use mpg321 to say it and then delete the temp file.
        $tempFile = tempnam(sys_get_temp_dir(), 'mp3');
        file_put_contents($tempFile, $result['AudioStream']);
        exec('mpg321 '.$tempFile);
        unlink($tempFile);
    }
    else
    {
        echo "Nothing to say";
    }