Codementor Events

Deep Dreaming with Node.js, or: Web Automation for Fun and Profit

Published Aug 07, 2015Last updated Apr 14, 2017
Deep Dreaming with Node.js, or: Web Automation for Fun and Profit

Introduction

If you've at all been near a computer during the last few weeks, there is no doubt you've seen the trippy, psychedelic pictures that are the result of Google's Deep Dream algorithm, which was recently released as Open Source. Even though the original source code claims rather cheerily that it is "designed to have as few dependencies as possible", it requires not only a working Python installation, but a number of additional modules, including the Caffe neural network library, which doesn't exactly provide a 1-click installer.

A couple of programmers immediately sensed an opportunity and started building web-based front-ends to Google's code that make producing Deep Dream images accessible to the average Facebook user.

However, if you're anything like me, you soon start getting annoyed with this sort of point-and-click interface, and wish there was an easy way to batch-process a bunch of files. This article will show you how to do just that.

NOTE: This tutorial is aimed at beginner to intermediate Node.js developer. If you know what npm is, you've seen a callback function before, and you can tell a HTTP GET request from a POST request, you should be able to follow along.

For the purposes of this tutorial, we'll use Dreamscope. Mainly because it's much faster than than Dream Deeply, even though (or maybe because) the latter is much more popular. Also, Dreamscope does not require an email address.

API? What API?

You might be thinking "great idea, Chris, I thought about this too, but when I went to the site there was no mention of a public API!"

Yes, that's true. But we don't need to stinkin' API documentation when we have our trusty Chrome Developer Tools.

First of all, it's obvious that there must be a public API. Otherwise, how would the web app be able to process the pictures for us?

  1. Open up Google Chrome and navigate to https://dreamscopeapp.com/create. Open the Web Inspector (View -> Developer -> Developer Tools), and switch to the "Network" tab.

  2. Now select an image using the file selector popup and choose any filter. Before you click "Submit", make sure you have the "Preserve log" option activated (see screenshot).

    Step1

  3. As the file is uploading and the browser window is updating, you'll see a bunch of activity flying by in the Network tab. Most of these will be GET requests, but there should be one POST request – this is the image upload that we're looking for.

    Step 2

  4. Click on this request to reveal the request inspector. Make a note of the request URL (highlighted in the screenshot). This is the exact URL that the image was posted to. We'll need this later.

    Step 3

  5. Also observe the Content-Type header. This indicates that this is a multipart request.

    Step 4

  6. Finally, observe the request payload. This is the data that's sent with the request. Notice how there's a filter parameter (which is set to art_deco), and a image parameter, which contains our filename (the actual file data, which is binary, is not shown, because it would be too long and not very interesting).

    Step 5

  7. Now keep scrolling down in the file list on the left. Notice how at the end of the list, there is a single filename that's repeated several times. Click on it, and switch to the "Preview" tab. Observe the fields highlighted in the following screenshot:

    Step 6

  8. Click on the last one of these requests. Again, in the "Preview" tab, observe the highlighted fields. Notice how processing_status has changed from 0 to 1? That's the cue that the server has finished rendering. Also notice how filtered_url now contains a filename, which matches the next and final request. That's the rendered image.

    Step 7

Putting the Pieces Together

So now we've reverse engineered the workflow of the web app, the only question that remains is how do we get that weird, random, 9-character filename that is being polled?

We would suspect that it's somewhere in the response to the initial POST request. That would be the most logical place for it. After all the web app somehow has to know where to poll the status information from. Unfortunately, when I tried to switch to the "Preview" tab for that request, I got an error message instead. So I attempted to make the same request from the command line, using curl:

curl -XPOST https://dreamscopeapp.com/api/images -F "filter=art_deco" -F "image=@/Users/chris/Pictures/IMG_2647.jpg"

Here's the response (pretty printed for easier reading):

{
    "profile": {
        "username": "Dreamscope"
    },
    "processing_status": 0,
    "original_url": "https://dreamscope-prod-v1.s3.amazonaws.com/images/e95adbce-caba-4214-9174-e2a715704187.jpeg",
    "profile_id": null,
    "private": false,
    "friendly_date": "just now",
    "last_modified": "2015-07-24 22:38:18.446238+00:00",
    "filtered_thumbnail_url": "",
    "filter_name": "art_deco",
    "description": "",
    "filtered_url": "",
    "uuid": "4bh1cFmnSF",
    "created": "2015-07-24 22:38:18.446223+00:00",
    "title": "",
    "nsfw": true
}

Looks like this is the same JSON data as we've seen the polling responses. Notice the uuid field? That looks like it has the exactly same format as the polling URL – ten random characters.

Quick Summary

Let's look at what we've learned so far. Creating a deep dream picture involves the following steps:

  1. Upload the image by making a POST request with the image and filter name to https://dreamscopeapp.com/api/images.
  2. Grab the uuid from the JSON response and append it to the initial URL to obtain the poll URL.
  3. Repeatedly issue GET requests to this URL, until the processing_status field has a value of 1.
  4. Download the rendered image from the filtered_url.

Let's Write Some Code!

Now that we know what the API looks like, we're ready to start our project. First create a new Node.js project in an empty directory by typing npm init and following the prompts. It doesn't really matter what you answer in each step, you can always change your answers in package.json later.

Next, we're going to install the superagent package. This is basically curl for Node.js, a very powerful package that lets us craft almost any HTTP request we want using a fluent API, similar to jQuery.

Now we're ready to write some code. Open a new file called dreamscopeapp.js and type or paste the following code:

var request = require('superagent');
var path    = require('path');
var fs      = require('fs');

var debug = function() {
    if (process.env.DEBUG) console.log.apply(null, arguments);
};

// parameters

var filter = 'art_deco';
var filename = 'IMG_2271.jpg';
var outputFilename = path.join(path.dirname(filename),
      path.parse(filename).name + '-filtered-' + filter + path.extname(filename)
    );

var url = 'https://dreamscopeapp.com/api/images';

That's all the information we need to make the requests: the filename, the filter name, and the URL.

We also need somewhere to store the output file, so we compute a new name based on the original file.

The debug function is just a simple helper that lets us log additional output to the console when running the program with the DEBUG environment variable set.

Next, we'll perform the actual request:

// make request
request
    .post(url)                    // this is a POST request
    .field('filter', filter)      // the "filter" parameter
    .attach('image', filename)    // attach the file as "image"
    .end(function(err, res) {     // callback for the response

    if (err) return console.log(err); // log error and quit

    debug(res.headers);
    debug(res.body);

    // compute the polling URL
    var poll_url = url + '/' + res.body.uuid;

    // This function calls itself repeatedly to check the processing_status
    // of the image until the filtered image is available.
    // When the image has finished processing, it will download the result.
    var poll = function() {
        request.get(poll_url, function(err, res) {
            if (!err && res.statusCode == 200) { 
                debug(res.headers);
                debug(res.body);

                var body = res.body;
                
                // check if processing has finished
                if (body.processing_status == 1 && body.filtered_url) {
                    console.log("Done.");
                    console.log("Downloading image...");

                    // download filtered image and save it to a file
                    request
                        .get(body.filtered_url)
                        .pipe(fs.createWriteStream(outputFilename))
                        .on('finish', function() {
                            console.log("Wrote " + outputFilename);
                        });
                } else {
                    // still processing – we'll try again in a second
                    process.stdout.write(".");
                    setTimeout(poll, 1000);
                }
            } else { // log error
              console.log(err);
            }
        });
    };

    // Start polling
    process.stdout.write("Processing...");
    poll();
});

A quick summary of what's happening here: first, we make a POST request to https://dreamscopeapp.com/api/images. We attach the image we want to process and specify the filter. In the callback, after checking for errors, we grab the uuid of the uploaded image and use it to compute the polling URL. Then, we call our poll function which reads from the polling URL and checks if the image has finished processing (i.e. processing_status is 1). If yes, we make another GET request to download the image and write it to a file. If no, we set a timeout to poll again in a second.

Please note that there is no point in lowering that timeout. It's not going to make your image process faster. In fact, it will just create more load on the server. The Dreamscope web app uses the same timeout, so it should be safe.

Run the code by typing node dreamscopeapp.js. It should show Processing... on the screen, and for a while, keep adding dots about once per second to indicate that it's polling for updates. Eventually, it should print Downloading..., and then exit.

Conclusion

What I've shown you here is much more than just turning pictures of your aunt into psychedelic dog faces. Using the same process, you can automate many other websites, as long as they don't require a captcha.

This knowledge can also come in handy when writing automated tests – submitting a form directly in the way we've shown here is much faster than running a headless browser like PhantomJS or CasperJS, not to mention Selenium, which launches and controls a full web browser.

Discover and read more posts from Christoph Wagner
get started
post commentsBe the first to share your opinion
Christian Heinirich
8 years ago

I get this error

{ [Error: Internal Server Error]
original: null,
response:
Response {
domain: null,
_events: {},
_eventsCount: 0,
_maxListeners: undefined,
res:
IncomingMessage {
_readableState: [Object],
readable: false,
domain: null,
_events: [Object],
_eventsCount: 4,
_maxListeners: undefined,
socket: [Object],
connection: [Object],
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: ‘1.1’,
complete: true,
headers: [Object],
rawHeaders: [Object],
trailers: {},
rawTrailers: [],
upgrade: false,
url: ‘’,
method: null,
statusCode: 500,
statusMessage: ‘Internal Server Error’,
client: [Object],
_consuming: true,
_dumped: false,
req: [Object],
text: ‘<h1>Server Error (500)</h1>’,
read: [Function],
body: undefined },
request:
Request {
domain: null,
_events: [Object],
_eventsCount: 1,
_maxListeners: undefined,
_agent: false,
_formData: [Object],
method: ‘POST’,
url: 'https://dreamscopeapp.com/a…,
_header: [Object],
header: [Object],
writable: true,
_redirects: 0,
_maxRedirects: 5,
cookies: ‘’,
qs: {},
qsRaw: [],
_redirectList: [],
_streamRequest: false,
req: [Object],
protocol: ‘https:’,
host: ‘dreamscopeapp.com’,
_callback: [Function],
res: [Object],
response: [Circular],
_timeout: 0,
called: true },
req:
ClientRequest {
domain: null,
_events: [Object],
_eventsCount: 3,
_maxListeners: undefined,
output: [],
outputEncodings: [],
outputCallbacks: [],
outputSize: 0,
writable: true,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
useChunkedEncodingByDefault: true,
sendDate: false,
_removedHeader: [Object],
_contentLength: null,
_hasBody: true,
_trailer: ‘’,
finished: true,
_headerSent: true,
socket: [Object],
connection: [Object],
_header: ‘POST /api/images HTTP/1.1\r\nHost: dreamscopeapp.com\r\nAccept-Encoding: gzip, deflate\r\nUser-Agent: node-superagent/2.0.0\r\ncontent-type: multipart/form-data; boundary=--------------------------929542548014952555818412\r\nContent-Length: 400659\r\nConnection: close\r\n\r\n’,
_headers: [Object],
_headerNames: [Object],
_onPendingData: null,
agent: [Object],
socketPath: undefined,
method: ‘POST’,
path: ‘/api/images’,
parser: null,
res: [Object] },
links: {},
text: ‘<h1>Server Error (500)</h1>’,
body: {},
files: {},
buffered: true,
headers:
{ ‘content-type’: ‘text/html; charset=utf-8’,
date: ‘Sun, 26 Jun 2016 06:29:45 GMT’,
server: ‘nginx/1.4.6 (Ubuntu)’,
vary: ‘Cookie’,
‘x-frame-options’: ‘DENY’,
‘x-xss-protection’: ‘1; mode=block’,
‘content-length’: ‘27’,
connection: ‘Close’ },
header:
{ ‘content-type’: ‘text/html; charset=utf-8’,
date: ‘Sun, 26 Jun 2016 06:29:45 GMT’,
server: ‘nginx/1.4.6 (Ubuntu)’,
vary: ‘Cookie’,
‘x-frame-options’: ‘DENY’,
‘x-xss-protection’: ‘1; mode=block’,
‘content-length’: ‘27’,
connection: ‘Close’ },
statusCode: 500,
status: 500,
statusType: 5,
info: false,
ok: false,
redirect: false,
clientError: false,
serverError: true,
error:
{ [Error: cannot POST /api/images (500)]
status: 500,
text: ‘<h1>Server Error (500)</h1>’,
method: ‘POST’,
path: ‘/api/images’ },
accepted: false,
noContent: false,
badRequest: false,
unauthorized: false,
notAcceptable: false,
forbidden: false,
notFound: false,
charset: ‘utf-8’,
type: ‘text/html’,
setEncoding: [Function: bound],
redirects: [] },
status: 500 }

Christian Heinirich
8 years ago

Hi does this still work?I’m getting alot of errors when I run …?

John Panagiotopoulos
9 years ago

Great article Christoff, i like how you got into the trouble of breaking down the logic behind the code & the reverse engineering process
Only thing i had to do to make it work is to change " body.processing_status ==1 " to " body.processing_status > 0 " , since the status returned was always 2 in my case

Show more replies