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?
-
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. -
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).
-
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 onePOST
request – this is the image upload that we're looking for. -
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.
-
Also observe the
Content-Type
header. This indicates that this is a multipart request. -
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 toart_deco
), and aimage
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). -
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:
-
Click on the last one of these requests. Again, in the "Preview" tab, observe the highlighted fields. Notice how
processing_status
has changed from0
to1
? That's the cue that the server has finished rendering. Also notice howfiltered_url
now contains a filename, which matches the next and final request. That's the rendered image.
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:
- Upload the image by making a
POST
request with the image and filter name tohttps://dreamscopeapp.com/api/images
. - Grab the
uuid
from the JSON response and append it to the initial URL to obtain the poll URL. - Repeatedly issue
GET
requests to this URL, until theprocessing_status
field has a value of1
. - 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.
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 }
Hi does this still work?I’m getting alot of errors when I run …?
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