Google Speech API – Full Duplex PHP Version

So this is a follow up to my post a while ago, talking about how to use the Google Speech Recognition API built in to Google Chrome.

Since my last post, Chrome has had some significant upgrades to this feature- specifically around the length of audio you can pass to the API. The old version would only let you pass very short clips (only a few seconds), but the new API is a full-duplex streaming API. What this means, is that it actually uses two HTTP connections- one POST request to upload the content as a “live” chunked stream, and a second GET request to access the results, which makes much more sense for longer audio samples, or for streaming audio.

I created a simple PHP class to access this API; while this likely won’t make sense for anybody that wants to do a real-time stream, it should satisfy most cases where people just want to send “longer” audio clips.

Before you can use this PHP class, you must get a developer API key from Google. The class does not include one, and I cannot give you one- they’re free, and easy to get just go to the Google APIs site, and sign up for one.

google_apis

Then download the class below, and start with a simple example:

<? 
require 'google_speech.php';

$s = new cgoogle_speech('put your API key here'); 

$output = $s->process('@test.flac', 'en-US', 8000);      

print_r($output);
?>

Audio can be passed as a filename (by prefixing the ‘@’ sign in front of the file name), or by passing in raw FLAC content. The second argument is an IETF language tag. I’ve only been able to test with both English and French, but I assume others work. It defaults to ‘en-US’. The third argument is sample rate, it defaults to 8000.

** Your sample rate must match your file- if it doesn’t, you’ll either get nothing returned, or you’ll get a really bad transcription. **

The output will return as an array, and should look something like this:

Array
(
    [0] => Array
        (
            [alternative] => Array
                (
                    [0] => Array
                        (
                            [transcript] => my CPU is a neural net processor a learning computer
                            [confidence] => 0.74177068
                        )
                    [1] => Array
                        (
                            [transcript] => my CPU is the neuron that process of learning
                        )
                    [2] => Array
                        (
                            [transcript] => my CPU is the neural net processor a learning
                        )
                    [3] => Array
                        (
                            [transcript] => my CPU is the neuron that process a balloon
                        )
                    [4] => Array
                        (
                            [transcript] => my CPU is the neural net processor a living
                        )
                )
            [final] => 1
        )
)

Get the PHP class here: http://mikepultz.com/uploads/google_speech.php.zip

Net_DNS2 Version 1.3.1 Released

I’ve released version 1.3.1 of the PEAR Net_DNS2 library- you can install it now through the command line PEAR installer:

pear install Net_DNS2

Download it directly from the Google Code page here.

Or, you can also add it to your project using composer.

Version 1.3.1

  • added the Net_DNS2_Packet_Request and Net_DNS2_Packet_Response objects to the Net_DNS2_Exception object
  • added support in the TSIG class for SHA algorithms (requires the hash extension, which is included in PHP >= 5.1.2), patch provided by Manuel Mausz
  • added support for the NID, L32, L64, and LP DNS RR’s (RFC6742)
  • lots of phpcs cleanup

Mining Twitter API v1.1 Streams from PHP – with OAuth

This is a quick update to my post about a year ago, with details on how to mine Twitter streams in real-time using PHP. This new code includes updates for the v1.1 API, including authentication using OAuth.

The first thing you need to do is sign in to the Twitter developer portal with your Twitter account here: https://dev.twitter.com/user/login twitter_account

Once you’ve logged in, click on your profile icon in the top right hand corner, select
“My applications”, and create a new application if you don’t already have one.

Select the option to create the access token as well, as the requests need to be signed by a Twitter account.

The Code

ctwitter_stream.php

class ctwitter_stream
{
    private $m_oauth_consumer_key;
    private $m_oauth_consumer_secret;
    private $m_oauth_token;
    private $m_oauth_token_secret;

    private $m_oauth_nonce;
    private $m_oauth_signature;
    private $m_oauth_signature_method = 'HMAC-SHA1';
    private $m_oauth_timestamp;
    private $m_oauth_version = '1.0';

    public function __construct()
    {
        //
        // set a time limit to unlimited
        //
        set_time_limit(0);
    }

    //
    // set the login details
    //
    public function login($_consumer_key, $_consumer_secret, $_token, $_token_secret)
    {
        $this->m_oauth_consumer_key     = $_consumer_key;
        $this->m_oauth_consumer_secret  = $_consumer_secret;
        $this->m_oauth_token            = $_token;
        $this->m_oauth_token_secret     = $_token_secret;

        //
        // generate a nonce; we're just using a random md5() hash here.
        //
        $this->m_oauth_nonce = md5(mt_rand());

        return true;
    }

    //
    // process a tweet object from the stream
    //
    private function process_tweet(array $_data)
    {
        print_r($_data);

        return true;
    }

    //
    // the main stream manager
    //
    public function start(array $_keywords)
    {
        while(1)
        {
            $fp = fsockopen("ssl://stream.twitter.com", 443, $errno, $errstr, 30);
            if (!$fp)
            {
                echo "ERROR: Twitter Stream Error: failed to open socket";
            } else
            {
                //
                // build the data and store it so we can get a length
                //
                $data = 'track=' . rawurlencode(implode($_keywords, ','));

                //
                // store the current timestamp
                //
                $this->m_oauth_timestamp = time();

                //
                // generate the base string based on all the data
                //
                $base_string = 'POST&' . 
                    rawurlencode('https://stream.twitter.com/1.1/statuses/filter.json') . '&' .
                    rawurlencode('oauth_consumer_key=' . $this->m_oauth_consumer_key . '&' .
                        'oauth_nonce=' . $this->m_oauth_nonce . '&' .
                        'oauth_signature_method=' . $this->m_oauth_signature_method . '&' . 
                        'oauth_timestamp=' . $this->m_oauth_timestamp . '&' .
                        'oauth_token=' . $this->m_oauth_token . '&' .
                        'oauth_version=' . $this->m_oauth_version . '&' .
                        $data);

                //
                // generate the secret key to use to hash
                //
                $secret = rawurlencode($this->m_oauth_consumer_secret) . '&' . 
                    rawurlencode($this->m_oauth_token_secret);

                //
                // generate the signature using HMAC-SHA1
                //
                // hash_hmac() requires PHP >= 5.1.2 or PECL hash >= 1.1
                //
                $raw_hash = hash_hmac('sha1', $base_string, $secret, true);

                //
                // base64 then urlencode the raw hash
                //
                $this->m_oauth_signature = rawurlencode(base64_encode($raw_hash));

                //
                // build the OAuth Authorization header
                //
                $oauth = 'OAuth oauth_consumer_key="' . $this->m_oauth_consumer_key . '", ' .
                        'oauth_nonce="' . $this->m_oauth_nonce . '", ' .
                        'oauth_signature="' . $this->m_oauth_signature . '", ' .
                        'oauth_signature_method="' . $this->m_oauth_signature_method . '", ' .
                        'oauth_timestamp="' . $this->m_oauth_timestamp . '", ' .
                        'oauth_token="' . $this->m_oauth_token . '", ' .
                        'oauth_version="' . $this->m_oauth_version . '"';

                //
                // build the request
                //
                $request  = "POST /1.1/statuses/filter.json HTTP/1.1\r\n";
                $request .= "Host: stream.twitter.com\r\n";
                $request .= "Authorization: " . $oauth . "\r\n";
                $request .= "Content-Length: " . strlen($data) . "\r\n";
                $request .= "Content-Type: application/x-www-form-urlencoded\r\n\r\n";
                $request .= $data;

                //
                // write the request
                //
                fwrite($fp, $request);

                //
                // set it to non-blocking
                //
                stream_set_blocking($fp, 0);

                while(!feof($fp))
                {
                    $read   = array($fp);
                    $write  = null;
                    $except = null;

                    //
                    // select, waiting up to 10 minutes for a tweet; if we don't get one, then
                    // then reconnect, because it's possible something went wrong.
                    //
                    $res = stream_select($read, $write, $except, 600, 0);
                    if ( ($res == false) || ($res == 0) )
                    {
                        break;
                    }

                    //
                    // read the JSON object from the socket
                    //
                    $json = fgets($fp);

                    //
                    // look for a HTTP response code
                    //
                    if (strncmp($json, 'HTTP/1.1', 8) == 0)
                    {
                        $json = trim($json);
                        if ($json != 'HTTP/1.1 200 OK')
                        {
                            echo 'ERROR: ' . $json . "\n";
                            return false;
                        }
                    }

                    //
                    // if there is some data, then process it
                    //
                    if ( ($json !== false) && (strlen($json) > 0) )
                    {
                        //
                        // decode the socket to a PHP array
                        //
                        $data = json_decode($json, true);
                        if ($data)
                        {
                            //
                            // process it
                            //
                            $this->process_tweet($data);
                        }
                    }
                }
            }

            fclose($fp);
            sleep(10);
        }

        return;
    }
};

The “process_tweet()” method will be called for each matching tweet- just modify that method to process the tweet however you want (load it into a database, print it to screen, email it, etc). The keyword matching isn’t perfect- if you search for a string of words, it won’t necessarily match the words in that exact order, but you can check that yourself from the process_tweet() method.

Then create a simple PHP application to run the collector:

require 'ctwitter_stream.php';

$t = new ctwitter_stream();

$t->login('consumer_key', 'consumer secret', 'access token', 'access secret');

$t->start(array('facebook', 'fbook', 'fb'));

You’ll need to provide the Consumer Key, Consumer Secret, Access Token, and the Access Secret, all of which are available from the Details section of your Application.

This new class uses the PHP hash_hmac() function for OAuth, which is available only in PHP 5.2.1 and up, and in the PECL hash extension 1.1 and up.

You can also Download the file here: http://mikepultz.com/uploads/ctwitter_stream.php.zip

The Decline of Customer Service – and How It’s Mostly the Consumers Fault

I recently wrote an article about my experiences dealing with Wind Mobile’s Customer Service, while I was trying to get a cell phone set up for my brother.

In the end, Wind admitted that they couldn’t actually provide service for my brother in his area, but rather than refunding his money, they offered him a $100 credit, but only if he remained a customer… which we obviously declined.

After writing the article, we received thousands of supportive messages from people all over the world, through Facebook, Twitter, Reddit, email, and comments on my article. I was blown away by the sheer magnitude of the response from the masses- I couldn’t be more grateful.

But I did notice two overwhelming themes in the messages I received, and both point to a very sad state for Customer Service.

The first type: “I know how you feel, the same thing happened to me.”

The “misery loves company” type; I received message after message from other Wind customers (sometimes they weren’t even Wind customers) about how they were mistreated in a similar fashion- and sometimes the only common element in our two stories, was that we both felt ripped off.

customer_service_1

As a consumer, we felt like the company took advantage of us, or wasn’t fair, and we just had to accept that and move on- we had no recourse.

The second type: “You should have known better”

This was actually a big surprise to me. I’m a pretty pessimistic person as it is, but I was blown away by the number of people that blamed me, and said “that I should have known better than to trust Wind”, or “why would you believe what they told you?”

I believe that consumers should be smart about their purchases; they should do their homework and understand what they’re getting in to when they sign something. But I also think that companies have a responsibility to be honest about their products and services and own up to it when they can’t or won’t deliver.

In my case, I had Wind Mobile insisting over the course of a month, that the cell coverage in my brothers area was “good, and definitely not the problem”, only to find out in the end, that it’s NOT good, and WAS definitely the problem.

customer_service_2

Even with their lies, or at best, completely uninformed (intentionally or otherwise) staff, I had people saying that I should have known better than to believe them.

Is that really where we are? Consumers being trained to not trust anything that comes out of a company representative’s mouth, and if we fall for it, it’s our own fault for believing them?

Good Customer Service shouldn’t be a “niche” market for smaller guys to distinguish themselves.

Not a huge surprise, but the bigger companies are the worst offenders. It doesn’t really affect them as much if they lose a customer here or there- it’s the CODB, and in a lot of cases, the customer needs the service more than the company needs the customer. So consumers feel trapped- and simply put up with whatever the company tries to get away with.

The reality is, that if enough customers refuse to accept what they’re handing out; choose not to accept their excessive pricing, their arbitrary rules, and their poor customer service, we could make a difference; we could force them to change and put honesty before profits- or at least, “more’ of the profits towards being honest and fair.

… but we won’t. Most people, after years of putting up with it, don’t believe they can actually make a difference, so they become complacent and apathetic, and sometimes, in an ironic, Stockholm syndrome-like twist, evangelists of the shitty things companies do, triggering them to write comments about how people should have known better.

customer_service_3

This type of thinking moves a large portion of blame on the consumer, as it tells these companies that’s it’s “OK”- we’ve developed a coping mechanism to deal with their recidivism, and they’re more than welcome to ratchet up the shit they want us to eat.

Wind Mobile Support Nightmare for my Autistic Brother

This is a TL;DR version of a post I put up on Monday- you can read the whole article here:

http://mikepultz.com/2013/04/left-blowing-in-the-wind-how-wind-mobile-took-my-autistic-brother-for-a-ride/

In 2009 my autistic brother moved from Vancouver to Toronto to come live with me, after our mother passed away in the summer. Part of my responsibility for him, was to get him set up on his own and independent- which included a monthly budget he could stick to.

On March 9th, I set up a Wind Mobile account for my brother- this was the best option for him because:

          1. It’s the cheapest mobile option out there.
          2. They have unlimited plans, so I don’t need to worry about him generating massive over-usage charges each month (i.e.- stick to his budget).

My brother saved up the extra $50 he has each month (he’s on ODSP), for 5 months to be able to afford the plan and half the cost of the phone (on a Windtab).

It took a little while to figure out (mostly because he’s autistic- troubleshooting is not high on his list of skills) that the phone was dropping calls when he was at his house (even outside his house, where he got full bars). So March 15th I phoned into Wind support for the first time.

After 9 calls to Wind support over the course of a month, where I experienced repeated hang-ups and delays for them to respond to me, they finally admitted to me that the area was not well covered, and that the problem was their network, and they have plans of putting in more cell towers “at some point in the future, but could be years”. Every agent I spoke to before this last one, insisted that the area was well covered, and that the dropped calls must be something else, that they insisted they could fix.

coverage_area

My brother needs a phone now- he can’t wait “years” for them to upgrade their network- so I asked to be transferred to somebody to cancel the account.

Unfortunately, because the Wind Mobile support process took more than 14 days to come to a conclusion (even thought I followed THEIR rules- THEIR support process), my brother no longer had the option of returning the phone, but was forced into paying out the remaining balance for the phone- another $250; money that he does NOT have; and if he didn’t pay out the balance, they would refuse to cancel the account- so monthly fees for a service that does not work.

If they had told us day 1 that the area was not well covered- we wouldn’t have signed up; but instead, they repeatedly lied about the coverage, insisting that the issues were something else. I can only assume that this was on purpose.

Wind Mobile needs to take responsibility for their failed support process, and their agents who are misinforming the public. But instead of this, they’ve offered my brother $100 credit if he comes back as a customer- a credit for a service that does not work.

So far, over 80,000 people have read my main article, over 10,000 in Canada alone- not to mention people on reddit, facebook, and twitter- with thousands of people commenting and asking Wind to do the right thing.