How My Automated Irish Literature Social Media Accounts Are Generated

If you're looking for non-technical information about Irish Lit AutoTweets, you should go here, instead. The document you're reading right now contains technical information about how the tweets are generated. If you'd like a good overview on how Markov chains work, Jeff Atwood has written a highly accessible introduction.

The Short Version

I exported all of the course-related email that I wrote during the quarter into a folder on my local hard drive, saved the relevant web pages as text in the same folder, copied and pasted the course Twitter stream into a text file in that folder, and saved a text-only version of student paper comments (yes, into the same folder). Then I massaged the data in various ways to make it more amenable to text processing and to remove sensitive information (primarily student names and grades).

The tweets themselves (and the longer Tumblr discourses) are generated using a program called DadaDodo, which generates text based on an analysis of existing text that transforms sentences into Markov chains. This involves two steps: first, I generated compiled probability data from the corpus using DadaDodo and saved the compiled data; then, I opened a new Twitter account, wrote a script to generate sentences based on that compiled data and upload individual selections to that Twitter account, installed it as a cron job on my laptop that runs six times a day, and wrote a couple of web pages to describe what happens. You're reading one of those web pages now.

After this account had been running for about two years, I went through one of my periodic cycles of looking comparatively closely at what the account was doing as it ran along on its merry way, and it occurred to me that it's generating an awful lot of text that simply disappears into the void, mostly because the text-generation process often generates text that doesn't fit into Twitter's 140-character limit. So I arranged for the wasted text to be captured by another script, which periodically posts longer discourses based on the same textual corpus to a new Tumblr account that I opened.

You can download and use either script, if you'd like, subject to certain restrictions; read on for details. If you find it useful, I'd be grateful if you'd Flattr me. And you should tweet at me (or find another way to reach me) if you do anything interesting with it or have input or ideas.

The (Much) Longer Version

This script is run under Linux Mint 17.1 (Rebecca), though it should be possible to make this methodology for generating Twitter content work, with necessary adaptations, on any Unix-like operating system, and should work more or less as described under any Debian-derived Linux system (Linux Mint is one; so is Ubuntu).

Assembling the Base Corpus

DadaDodo needs something to analyze and use as the basis of the text it generates; I refer to this as the base corpus. Text used as the base corpus for this project consisted of all of the writing I did that I had captured during the quarter, and it came from several sources:

All of these documents were aggregated into a single folder, then concatenated with the standard POSIX cat command to produce a the first draft of the base corpus. After that, there was a long and boring period of searching and replacing to eliminate undesirable text that I didn't want used as the basis of the text that the script generates (URLs, student names, smart quotes, grade-related information, carriage returns, double spaces, etc. etc. etc.) Searching, replacing, and other editing were largely done with Bluefish, which is a pretty good text editor that supports searching for regular expressions and understands POSIX backslash sequences. I installed DadaDodo (sudo apt-get install dadadodo under Linux Mint). I called the single-file base corpus 150.txt because the course for which I did the teaching was, of course, English 150.

I created a folder in my file system at an appropriate place in the filesystem hierarchy, several folders beneath my Documents folder, then created a symbolic link with a short name to that folder at the root level of my file system (it's at /150) to save myself some typing. Then I moved the base corpus so that it was /150/150.txt. Once that was done, I compiled the textual corpus into a set of Markov chains with DadaDodo: dadadodo -w 10000 -o chains.dat -c 1 150.txt outputs chains.dat, which is then the precompiled Markov chains that the script uses to generate chunks of text during its regular runs several times a day. (This isn't, strictly speaking, necessary; it's possible to just have DadaDodo re-analyze the entire base corpus from scratch each time it runs. But, since the base corpus is almost 650K of text, it saves some time on each script run to prepare the set of Markov chains once, in advance, instead of redoing the same work multiple times per day.)

Automating Text Generation

Once the textual corpus was assembled, I signed up for a new Twitter account for the project, logged in, and edited the profile; the website listed on the profile is a short link to this page, the one that you're reading now. Then I wrote and tested a script that calls DadaDodo repeatedly to generate chunks of text until the generated chunk is acceptable, then sends that acceptable chunk to Twitter. The first version of the script was a Unix-style shell script that executed under bash; you can see the description and write-up for this particular script here, if you'd like. It worked quite well for all of the nearly two years some version of it was in use and was, at least at the time that it was retired, still a viable solution.

Nevertheless, on 15 September 2015, I replaced the shell script with a Python 2.X script (more specifically, it was written under 2.7.6) that does the same thing. Part of my motive was simply to give myself some practice using Python; part of my motive was a desire to develop the script a bit further (and Bash shell scripts are a real pain to write; a complete re-write in Python is a small investment compared to the aggregate time of maintaining and further developing a Bash script over a longer period of time. And part of my motive was to remove the dependency on TTYtter, which I was previously using to send the tweets to Twitter. TTYtter is a command-line Twitter client that can be scripted; it's a great program, but there were two problems with using it. One is that it's overkill; it does much more than I actually need to do with it. Depending on an 8000-line Perl script seemed unnecessary when all I want to do can be accomplished with fewer than a dozen lines of Python. Too, an early-2014 email from TTYtter developer Cameron Kaiser in response to a question I'd asked him said that he no longer used Twitter and was unlikely to continue to do substantial work on TTYtter. Depending on a project that may be gradually drifting toward orphanhood seemed a bad move to me; in the long term, I needed either to find a replacement that could be used from a Bash script or learn to interact with Twitter in Python using OAuth. Since I'm learning Python anyway, and thought a project would be useful, that's the way I went. Interacting with Twitter in Python is easy using any of several libraries; I decided to go with tweepy. It's not installed by default, but pip install tweepy takes care of that easily enough.

Once all the pieces were in place, I rewrote the previous Bash script (again, details are here) in Python (I'm running 2.7.6 for this script; I haven't yet tried moving it to Python 3, although that will probably happen eventually), as generate.py, and tested it. It runs through a few steps every time it's run:

  1. Parses the command line that's passed to it. It understands several command-line options; try running it as ./generate.py --help to see an explanation.
  2. Calls dadadodo, stripping the leading and trailing spaces that the program tends to include.
  3. Checks to see if the resulting text is within acceptable length parameters. Of course, Twitter's famous 140-character limit is a hard upper limit that winds up being kind of annoying sometimes, but I also enforce a minimal limit (currently 46 characters) because the very short tweets that DadaDodo generates from the base corpus tend to be rather dull, in my opinion. (Early tests of the script produced the tweet History. about one out of every five times.) If the automatically generated tweet isn't an acceptable length, it just keeps trying until it generates one that is. (Since v1.2, if the script is run with the -x or --extra-material flags, it saves rejected tweets in a separate file.)
  4. Checks to see if the tweet has been posted before. DadaDodo repeats itself from time to time, so this script keeps a list of tweets that have been posted before (I refer to this as the tweet archive). If the tweet's not new, the script starts over and tries again. (Repeated tweets that are generated are not saved to the extra material archive, even if long and short tweets are being saved.)
  5. Sends the resulting new tweet of acceptable length to Twitter.
  6. Saves the new tweet to the tweet archive. As far as I can tell, there's no good reason to sort the tweet archive — it doesn't, for instance, seem to speed up the search that determines whether the tweet has been posted before — but sorting alphabetically is possible by running ./generate.py --sort-archive, if you're the kind of person who likes things to be sorted whether there's a benefit to it or not.

You can see the script on GitHub, and previous versions are available as branches; I (in)tend to tweak it from time to time. You're welcome to download and adapt and use the script yourself for your own purposes if you'd like, subject to certain conditions; it's licensed under a the GNU GPL v3 or, at your option, any later version. You'll need to make it executable (chmod +x generate.py), of course, or else to invoke the script name as an argument to Python on the command line, perhaps as python /150/generate.py. If you find this script useful, I'd be grateful if you'd Flattr me, though this is (of course!) not necessary. I'd love to hear about it if you do anything interesting with it, or if you have suggestions for improvements.

Automating the Script's Runs

Once the script was set up and worked effectively when I ran it manually, I installed it as a cron job (crontab -e) so that it runs periodically (I decided on five times a day, and later bumped that up to six). Here's the line from my cron file that I use as of the time of this writing:

45 0,4,8,12,16,20 * * * /home/patrick/.Enthought/User/bin/python /150/generate.py -v -v -v -a /150/tweets.txt -x /150/extras.txt

Which is to say that the script runs on my laptop at 12:45, 4:45, and 8:45 a.m., and 12:45, 4:45, and 8:45 p.m., posting a new tweet each time, saving that tweet to the tweet archive at /150/tweets.txt, and saving rejected material to /150/extras.txt. Provided that my laptop is on and connected to the Internet, of course. Once in a while, the extra material that's being collected is posted to the Automated Irish Lit Discourses Tumblr account ... about which I will say more in a bit.

I also installed it as an anacron job to make sure that it runs at least once a day if my laptop is turned on. Here is the relevant line from /etc/anacrontab:

1 20 IrishLitTweets.daily /150/generate.py

Some Notes on the Twitter Script

I often specify full paths in the script because cron jobs may not have a properly set-up environment that guarantees environment variables, such as $PATH, are properly set. I find it easier to just specify full paths than to keep a crontab PATH declaration in sync with one that's maintained in a .bashrc.

Here's what the switches for the the dadadodo invocation mean:

-c 1
Just generate one sentence.
-l /150/chains.dat
Don't use the original corpus; use the manually compiled statistical data instead. This is faster, though the script still runs fairly quickly without it. Still, I'd rather avoid wasting processor time unnecessarily; I'm often doing other things on my laptop when this script runs.
-w 10000
Use a really wide text wrap amount to make sure that DadaDodo doesn't wrap text at all.

Longer Discourses on Tumblr

Periodically, the extra material that builds up in the extra material archive gets posted to Automated Irish Literature Discourses on Tumblr. This is accomplished by another Python script, discourse-generate.py, which is also available on GitHub (and which was also written under Python 2.7.6).

discourse-generate.py lives in /IrishLitDiscourses on my hard drive; it's a simpler, less-robust, less-developed script that just looks at /150/extras.txt twice a day (4 a.m. and 4 p.m., Pacific time) and makes a decision about whether to post it. The script doesn't take command-line arguments or do enough error-checking; it just rolls the dice and, sometimes, posts. My initial observation was that it took too long to build up material for posting to the Tumblr account, so what I did was tweak the Twitter script so that (as of v1.3), instead of asking for a single sentence, it generates anywhere between one and six sentences when the -x or --extra-material-archive switches are specified. This means that tweets of two or more sentences now sometimes appear in the Twitter stream; but it also has the side effect of dumping much more material into the extra material archive, since collections of two to six sentences are more likely to be too long for Twitter than single sentences are.

More specifically, the Tumblr script decides whether to post by comparing a random number to a probability calculated based on the current length of the extra material archive. The script uses an exponential decay curve to affect how likely it is that the script posts: the more material is built up, the more likely it is that the material will be posted when the script rolls the dice to determine whether or not the material gets shipped out. If there are fewer than three thousand characters waiting to go out, the chances of them doing so on any iteration of the script are zero, but the probability doesn't actually reach certainty until there are a bit over 1.5 million characters waiting to go out. (Practically speaking, of course, running the script, and therefore rolling the dice, twice a day with gradually increasing probability of posting means that the extra material archive should never get anywhere near that large.)

When the script decides to post, it decides on a title (currently Discourse of [Date]), a slug (the end of the Tumblr post's URL), the text of a Twitter annoucement, and a set of tags for the post. Then it sends them off to Tumblr using the pytumblr library.

Overall, then, the process looks like this:

  1. The the Twitter script, /150/generate.py, runs six times a day, posting tweets to @IrishLitTweets. Extra material that's rejected for being the wrong length accumulates on my hard drive in /150/extras.txt.
  2. Twice a day, assuming my laptop is on and connected to the Internet at the right times, the Tumblr script runs and decides whether a random number between zero and one is or is not less than 1 − e (len − 3000) × 14000, where len is the current length of the extra material archive, in bytes. If the random number is less than the number from the probability calculation, then:
    1. The accumulated text gets shipped off to the Tumblr account with an appropriate title, slug, and set of tags.
    2. A tweet is posted to @IrishLitTweets that points to the new long discourse on Tumblr.
    3. The extra material archive gets emptied out.
    4. The extra material archive gradually gets filled up again as the Twitter script runs. The process repeats.

Here's the crontab line that runs the script twice a day:

0 4,16 * * * /home/patrick/.Enthought/User/bin/python /IrishLitDiscourses/discourse-generate.py

And here's the line from /etc/anacrontab line that makes sure the script runs at least once on any day that my laptop is on for at least half an hour:

1 30 IrishLitDiscourses.daily /home/patrick/.Enthought/User/bin/python /IrishLitDiscourses/discourse-generate.py

Guest Lecturers

I'm planning on posting other DadaDodo-generated discourses from time to time based on statistical analyses of other texts about Irish literature. It will be a bit before this happens, because there are other tweaks that I want to make first, but there's a lot of material out there that could be run through DadaDodo and used as the basis for these guest lectures by historical figures. It's likely, for instance, that an early set of guest lectures will be based on Matthew Arnold's The Study of Celtic Literature. Suggestions are welcome, especially if you can point to source texts for analysis; let me know on Twitter if you've got a good idea.

Reservations About the Current Setup

All of which is to say, again, that I'd love feedback if people have thoughts or ideas about how this could be done better.

Change History

All updates by Patrick Mooney. The latest stable version of the tweet-producing script is always on the master branch on the GitHub project. The version of the script actually producing the text is always the highest version-numbered branch on GitHub (it is pushed to GitHub when it first goes into testing; currently this is v1.2). The same is true of the master branch and highest-numbered branch (currently v1.0) of the script posting longer sections of text on Tumblr.