Catbot @ Bristol Shakespeare Festival
The sonnet-reciting catbot I created with (daughter) Elizabeth Walker recited 4114 lines of poetry, each one selected at random, as part of The Complete Reworks of Shakespeare, at the Bristol Shakespeare Festival on 30 June 2012. Here are four of those lines:
Catbot was very well received. Overheard:
“He’s great. Brilliant.”
“Absolutely superb!”
“Beautiful, fantastic.”
“Sweet!”
“I’ve seen worse” [acting]
“Sort of the reverse of monkeys typing Shakespeare, isn’t it?”
Emma Henry, Artistic Director of Bristol Shakespeare Festival, was particularly excited to finally meet catbot, as was the rest of the Festival team.

More pics & video here. The entire event was great – particularly Jeremy Routledge’s production ‘I had a dream that I can’t die’ was incredible – so well written and acted, it deserves multiple awards. See full programme of events on the day here.
Technical details
Catbot hardware is adapted from the Dagu ‘Playful Puppy’ robot; the back legs and tail were not used here. IR sensor eyes were considered, to respond to passers-by, but we decided best for catbot to instead recite continuously in order to attract people over. Audio came from a Jambox (an excellent little speaker – highly recommended!). Catbot was tethered via USB to a Macbook, which read random sonnet lines using a text-to-speech Applescript.
Two Processing sketches were running simultaneously – one read sonnets and sent characters out to the robot to make it move, and the other displayed text in a speech bubble on-screen. Why two? I timed everything in the first sketch using delay() statements, but these buggered the drawing of graphics. So… the first sketch reads a text file containing all the sonnets bunched together (this makes a nice rich dataset), pulls one line at random and writes it to a second text file. The Applescript reads this file, and the second Processing sketch reads this file to display the text in a speech bubble.
Here’s the Arduino sketch that lives on the ‘Magician’ Arduino-clone board on the robot:
#include // Import the servo library
Servo servoSR; // Make servo objects for all the servos - Shoulder Right, Elbow Right, etc
Servo servoER;
Servo servoEL;
Servo servoSL;
Servo servoHead;
Servo servoNeck;
int pos = 0; // variable to store servo position
void setup()
{
servoSR.attach(2); // Attach the servo objects
servoER.attach(3);
servoEL.attach(12);
servoSL.attach(13);
servoNeck.attach(5);
servoHead.attach(6);
Serial.begin(9600); // Open a serial connection with computer
servoSR.write(160); // 160 to 0 // Starting positions, the numbers adjusted based on how they were mounted and for realistic movement.
servoER.write(75); // 60 to 175
servoSL.write(10); // 10 to 150
servoEL.write(15); // 15 to 125
servoNeck.write(80); // 0 to 180
servoHead.write(170); // 100 to 180
}
void loop() {
if (Serial.available() > 0) { // Check and see if there is a command coming from the computer
int inByte = Serial.read(); // If so, read it into the variable called inByte.
if (inByte == 'A') { // If the incoming character is an A, run the function called strikeApose
strikeApose();
}
if (inByte == 'B') { // If the incoming character is a B, run the function called Rest.
rest();
}
}
}
void strikeApose() { // A function I created to pose the robot.
int shoulderRstart = servoSR.read(); // Read the current positions of the servos into these variables.
int elbowRstart = servoER.read();
int shoulderLstart = servoSL.read();
int elbowLstart = servoEL.read();
int headStart = servoHead.read();
int neckStart = servoNeck.read();
int shoulderRend = random(160); // These variables hold the positions to end on.
int elbowRend = random(115)+60;
int shoulderLend = random(140)+10;
int elbowLend = random(110)+15;
int headEnd = random(80)+100;
int neckEnd = random(180);
// The following just move from start position to end position. The delay is to wait for the servo to catch up. Because hardware can'
work as fast as software!
// First move the head, then the neck, then choose a random limb.
// HEAD
if (headEnd < headStart) { for(pos = headStart; pos >= headEnd; pos -= 3) {
servoHead.write(pos);
delay(15);
}
}
else {
for(pos = headStart; pos < headEnd; pos += 3) {
servoHead.write(pos);
delay(15);
}
}
// NECK
if (neckEnd < neckStart) { for(pos = neckStart; pos >= neckEnd; pos -= 3) {
servoNeck.write(pos);
delay(15);
}
}
else {
for(pos = neckStart; pos < neckEnd; pos += 3) {
servoNeck.write(pos);
delay(15);
}
}
int limb = random(4);
if (limb == 1) { // RIGHT SHOULDER
if (shoulderRend < shoulderRstart) { for(pos = shoulderRstart; pos >= shoulderRend; pos -= 3) {
servoSR.write(pos);
delay(15);
}
}
else {
for(pos = shoulderRstart; pos < shoulderRend; pos += 3) {
servoSR.write(pos);
delay(15);
}
}
}
if (limb == 2) { // RIGHT ELBOW
if (elbowRend < elbowRstart) { for(pos = elbowRstart; pos >= elbowRend; pos -= 3) {
servoER.write(pos);
delay(15);
}
}
else {
for(pos = elbowRstart; pos < elbowRend; pos += 3) {
servoER.write(pos);
delay(15);
}
}
}
if (limb == 3) { // LEFT SHOULDER
if (shoulderLend < shoulderLstart) { for(pos = shoulderLstart; pos >= shoulderLend; pos -= 3) {
servoSL.write(pos);
delay(15);
}
}
else {
for(pos = shoulderLstart; pos < shoulderLend; pos += 3) {
servoSL.write(pos);
delay(15);
}
}
}
if (limb == 4) { // LEFT ELBOW
if (elbowLend < elbowLstart) { for(pos = elbowLstart; pos >= elbowLend; pos -= 3) {
servoEL.write(pos);
delay(15);
}
}
else {
for(pos = elbowLstart; pos < elbowLend; pos += 3) {
servoEL.write(pos);
delay(15);
}
}
}
}
void rest() { // Function I created to make the robot go back to starting position
servoSR.write(160); // 160 to 0
servoER.write(75); // 60 to 175
servoSL.write(10); // 10 to 150
servoEL.write(15); // 15 to 125
servoNeck.write(80); // 0 to 180
servoHead.write(170); // 100 to 180
}
Here’s the Processing sketch that reads the sonnets and controls the robot:
import processing.serial.*; // We use serial library to talk to Arduino
Serial myPort; // Create new serial port object
PrintWriter output; // Create new object to write to external files
void setup() {
String portName = Serial.list()[0]; // Port 0 is my USB port for Arduino
myPort = new Serial(this, portName, 9600);
}
void draw() { // Note - no visual component here, only robot and separate bubble.pde
String lines[] = loadStrings("sonnets.txt"); // Read external plain text file which holds the lines of all Shakespeare's sonnets
output = createWriter("line.txt"); // Create new text file to hold one line
String theline = lines[int(random(lines.length))]; // Choose random line from sonnets
output.println(theline); // Write the line to text file
output.flush();
output.close();
//delay(2000);
open("/Users/kevin/Desktop/robot/speak.app"); // Applescript speaks text from the file
delay(1000); // Characters sent out via serial control robot movements, with some delays for timing.
myPort.write('A');
delay(1000);
myPort.write('A');
delay(1000);
myPort.write('A');
delay(2500);
myPort.write('B');
delay(2000);
}
Here’s the second Processing sketch that displays the speech bubble:
PImage bubble; // image object for speech bubble graphic
PFont font; // create new font object
int timer = millis(); // for timing display of bubble & text
String[] theline; // variable for holding incoming line to display
void setup() {
size(1280, 768);
bubble = loadImage("bubble.png");
bg = loadImage("bg.png");
font = loadFont("Handwriting-Dakota-72.vlw");
textFont(font);
textLeading(80);
fill(0);
theline = loadStrings("/Users/kevin/Desktop/robot/robot2/line.txt");
}
void draw() {
if (loadStrings("/Users/kevin/Desktop/robot/robot2/line.txt") != null) {
if (loadStrings("/Users/kevin/Desktop/robot/robot2/line.txt")[0].equals(theline[0])) {
// if external text is same, do nothing and wait for new
}
else {
theline = loadStrings("/Users/kevin/Desktop/robot/robot2/line.txt"); // load it up
timer = millis(); // start the timer
}
}
if (millis() - timer < 5500) { // max display time if (millis() - timer > 1000) { // min display time
image(bubble,0,0); // show the bubble graphic
text(theline[0], 250, 200, 800, 600); // show the text
}
}
else {
background(0); // after max time, remove the bubble
}
}
Finally, here’s the Applescript that reads the text:
open for access file "Macintosh HD:Users:kevin:Desktop:robot:robot2:line.txt" set wordContents to (read file "Macintosh HD:Users:kevin:Desktop:robot:robot2:line.txt") say wordContents close access file "Macintosh HD:Users:kevin:Desktop:robot:robot2:line.txt"
Comments are closed, but trackbacks and pingbacks are open.