Generate Songs with Markov Models using Server-Side Swift, Perfect, and Twilio SMS

March 14, 2019
Written by

generate-music-with-swift-twilio

I like music and coding and one way to make music with code is by predicting words and generating a new song. You can do this with Markov models, as introduced in this last Twilio blog post. Now let's learn how to train a model on a .txt file to generate a song and then generate another song or text via Twilio SMS with server-side Swift and Perfect.

Set Up

To code along with this post you should have the following:

  1. A Twilio account to buy a phone number
  2. ngrok, a tool for putting the app running on your local machine on the web
  3. Xcode

First, make a new Single View project in Xcode and run pod init on the command line in the directory where your Xcode project exists to create a Podfile in order to install the Markov Model library via CocoaPods, as further detailed in the first Markov model blog post. Open your Podfile and add pod "MarkovModel" before running pod install on the command line. Close Xcode, open the .xcworkspace file generated by CocoaPods, and create a Swift Playground in Xcode by clicking File->New->Playground. Save it in the top level of your project's directory. If you don't see the playground, drag it into the workspace like so:

The MarkovModel library says you can install it via SPM but there can be dependency errors. If you run into errors you can use CocoaPods, too. I described how to do this in the last blog post.

Download the rickroll.txt file. It contains the song this post uses to train the Markov model. You should save it under the Resources folder of your Playground.

rickroll gif

Train the Markov Model on a Text File

What is this text file containing a song for? It's to generate another song! First we'll make a helper method that takes in the first word of the text file to read, the length of the file, and "chain", which is the start of the generated text. The next state can be calculated by calling next on the given word in the text. This method loops through, generating the new words that will comprise our song.

import MarkovModel

func buildWords(with text: inout String, length: Int, chain: Matrix<String>) {
    var word = text
    (0...length).forEach { _ in
        if let next = chain.next(given: word, process: .weightedRandom) {
            text.append(" \(next)")
            word = next
        }
    }
}

Alas, this is not enough. Before the text file can be read, the input must be cleaned. BuildWords is called in this next method that reads the file from the Resources directory, replacing new lines with spaces. It then calls the MarkovModel library's process method to train the model on the text.

func buildText(starting: String, length: Int, file: String) -> String {
    let filename = file.split(separator: ".")
    var text = starting
    let filePath = Bundle.main.path(forResource: String(filename[0]), ofType: String(filename[1])) //get file from Resources
    let data = FileManager.default.contents(atPath: filePath!)
    let content = String(data: data!, encoding: .utf8)!
    let strings = content.replacingOccurrences(of: "\n", with: " ").components(separatedBy: " ")
    
    //train model and work on it all at once in a closure
    MarkovModel.process(transitions: strings) { matrix in
        buildWords(with: &text, length: length, chain: matrix)
    }
    return text
}


The model trains and works on the text file all at once in the above .process closure and then calls buildText to generate a new song on the trained model.

var firstWord = "We're"
let randomText = buildText(starting: firstWord, length: 20000, file: "rickroll.txt")
print(randomText)

The 20000 is just a large number that is the length of the file or greater. You can run this code in your Playground by clicking the arrow on the numbered line next to the print(randomText) line.

You should see a new generated song similar to Never Gonna Give You Up in the Xcode debug area as shown below.

If you get a "Couldn't lookup symbols" error, try cleaning and building the project with cmd-shift-k followed by cmd-shift-b before running the Playground.

You can train the Markov model on any song, like I'll Make a Man out of You from Disney's Mulan.

I&#39;ll make a Man out of you gif

Now let's set up a Twilio phone number to create an SMS-generated text file.

Setting up a Twilio Phone Number to Receive and Respond to SMS

text me gif

To receive and respond to messages you’ll need to create a Twilio account and buy a phone number (it’s free if you’re using the number to test your code during development).

Your Perfect app must be accessible from the web so Twilio can send requests to it. ngrok is a great free tool that lets you put a web server running on your local machine on the web. You’ll need to install it if you haven't already. Run the following command in your terminal:

ngrok http 8181

ngrok screenshot

With the publicly-accessible URL shown above, you can point the Twilio webhooks to your app by configuring your phone number. The image below shows you how to define a webhook that will be sent to the /sms endpoint of your app when a message comes in. 

Now your new Twilio number can receive text messages. These messages will make up the .txt file which we’ll use to train our Markov model.

Train the Markov Model on Twilio SMS-Generated Text

For more details on sending SMS with Perfect and Twilio, check out this blog post.

Create a new project with Swift Package Manager by running the following from the command line at the top level of your project directory: 

swift package init --type executable

In Package.swift, add .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.0") to dependencies and "PerfectHTTPServer" to target dependencies. Your file should then look something like this:

import PackageDescription

let package = Package(
    name: "your-proj",
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "your-proj",
            dependencies: ["PerfectHTTPServer"]),
        .testTarget(
            name: "your-proj-Tests",
            dependencies: ["your-proj"]),
    ]
)

If you get a No such module 'PackageDescription' error, run swift build on the command line anyways.

In Sources/your-proj/Main.Swift import Perfect and create the following function that takes in a string and saves it to a file called "userinput.txt".

import PerfectHTTP
import PerfectHTTPServer
import Foundation

func writeToFile(urlString: String) {
    let file = "userinput.txt"
    if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
        let path = dir.appendingPathComponent("/" + file)
        print(path)
        do {
            try urlString.write(to: path, atomically: false, encoding: String.Encoding.utf8)
        }
        catch {/* error handling here */}
    }
}

Add a getString helper method that takes in an array of strings and returns one string.

func getString(array : [String]) -> String {
    let stringArray = array.map{ String($0) }
    return stringArray.joined(separator: ".")
}

Create a route to listen to our Twilio server, receiving inbound text messages and saving them to an array that is written to our userinput.txt file.

var inputArr: [String] = []
var routes = Routes()
routes.add(method: .post, uri: "/sms", handler: {
    request, response in
    response.setHeader(.contentType, value: "text/xml")
    
    var body = request.param(name: "Body", defaultValue: "")
    if let bod = body {
        inputArr.append(bod + "\n")
        let respStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Message><Body>Got your message!</Body></Message></Response>"
        response.appendBody(string: respStr )
            .completed()
    }
    var joinedArr = getString(array: inputArr)
    print(joinedArr)
    writeToFile(urlString: joinedArr)
})

This route handler also sends an outbound response text and prints out the body of the inbound message. This way you can see what is in the text file that is used to train our Markov model.

Lastly, you need to launch the ngrok server, opening up the app to the web. Run the app by clicking on the line number corresponding to the final bracket in Playgrounds. Now, the Twilio number can receive and respond to inbound messages.

do {
    // Launch the HTTP server.
    try HTTPServer.launch(
        .server(name: "http://replace-with-your-ngrok-url.ngrok.io", port: 8181, routes: routes))
} catch {
    fatalError("\(error)") // fatal error launching one of the servers
}

Don't forget to replace the ngrok URL with your own! You can run the code by running swift run on the command line. When asked if you want to accept incoming network connections, click "Allow".

If you text your Twilio number you should see your inbound messages. Send some longer phrases (a few words, sentences) to fill up the text file until you think the file includes enough words.  Then, physically move your Documents//userinput.txt to your Playground Resources folder. This process is annoying, but I think Playgrounds's ability to quickly run Swift code makes it worth it despite its obvious limitations.

Now add the following code to your main Playground file to train the Markov model on that SMS-generated text file, replacing "replace-with-the-first-word-of-your-userinput.txt" with the first word of your userinput.txt.

var firstWord = "replace-with-the-first-word-of-your-userinput.txt"
let randomText = buildText(starting: firstWord, length: 50, file: "userinput.txt")
print(randomText)
}

Run the Playground and voila! You should see the SMS- and Markov-model- generated text.

voila gif

With the described setup you can use Perfect to receive and respond to messages and save these to a text file in Swift. You can keep on training your Markov model on other texts that aren't rickroll.txt, like I'll Make a Man out of You from Mulan.

The completed code can be found on GitHub here. Let me know in the comments or online what song you want to train a Markov model on and also what you're building with Swift!