Catbot @ Bristol Shakespeare Festival

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.

Emma Henry and catbot

 

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"