I created a little puns website on a whim, but also because I wanted to learn how to deploy a site using Python and Flask.
The website evolved backwards because my first instinct was to create an experience using data science techniques and statistical models but that turned out to be not as user-friendly as simply offering up the content and letting it shine.
The original version of the site worked like this:
- user sees a question and a form
- user inputs text into a form to guess the answer, which should contain a pun
- user sees the question and both actual and guessed answer, and gets success/failure confetti
- user then clicks on either a "correct" or "wrong" button in reaction to the success/failure confetti
- that final user feedback reloads the next question
The success/failure was either happy unicorn confetti , or sad lobster ones. Success was determined by an ensemble of models whose weighted average score had to pass a threshold. Those models would compute the similarity between the two answers in different ways, with simple text similarity measures or word embeddings, using transformers, and even phonetically, and the system of weights would adapt every time the user gave feedback about whether the success/failure confetti was correct or wrong, in a sense "training" the ensemble model over time.
The problem with all this wasn't the data science - it was the user experience. Forget for a second that it's a bit onerous and obscure to ask users to type in answers and later rate confetti reactions for the success of ensemble models in their weighted average evaluation of success or failure in... just explaining how the site works hurts most people , what was really wrongheaded is that it is way too hard to guess the answer! and most of the time a user would just get them sad lobsters. If we lower the threshold of success so more unicorn confetti are thrown, that just confuses the models and users who don't understand why there was success when there should've been a failure.
Given the bad UX, I decided to "rollback the site"... (fine, I didn't practice agile and deployed a more complex experience first so there was no rollback) to its current form:
- user sees a question and can press any key to see the answer
- user sees the answer and several emoji buttons the user use to "rate the pun"
- the pun rating reloads the next question
Not only is the flow a lot simpler and clearer, it's also just more fun. Instead of rating the performance of an ensemble of statistical models, the user is just rating the pun, interacting more with the actual content. The site is about puns - not "text similarity models"!
As for the confetti.... they went from two animals to a whole zoo.
Initially I'd throw the confetti every fifth or tenth answer but as I added animals, I decided to make the experience seem a bit more random. And by random I don't mean the mathematical definition , but the popular notion that random = "unexpected"... so for example, if users attempts guess which animal will be confettied next and when, that won't be so guessable and will keep users on their toes.
The site is deployed at an invite-only ip address for a select group of beta users since it costs to host the server and I'm at near capacity for that small server. The code here can serve as a bootstrap to deploying your own website with Python and Flask if you so desire.
Below are some behaviors of the frontend:
- signup:
- only possible when logged out
- flashes msg otherwise and redirects to
play
- flashes msg otherwise and redirects to
- fails if username exists
- redirects to login on success
- only possible when logged out
- login:
- only possible when logged out
- flashes msg otherwise and redirects to
play
- flashes msg otherwise and redirects to
- fails when password is wrong
- security implemented so only user knows password
- fails when username doesn't exist and redirects to
signup
- session inactivity will
logout
user
- only possible when logged out
- logout:
- not a route but a
base
template footer button - displayed when logged in only on all pages
- not a route but a
- play:
- goal of
signup
andlogin
- when logged in,
signup
orlogin
will redirect toplay
- goes through puns in order for every user
- shows only question and word count so user can try to guess
- any key redirects to
view_answer
- persist puns asked beyond single session so user can come back where she left off
- starts over when all puns have been shown
- goal of
- view answer:
- reveals answer and pun
- user must click on one of six "emoji buttons" to rate the pun
- feedback get recorded in the backend table
ratings
, and triggers newplay
- new
play
might or might not throw animal confetti depending on mysterious rules
- stats:
- display user stats in a simple column graph with counts for each rating + avg rating line
- displayed separately from
view_answer
so as not to influence user ratings too much
- many of the puns were originally sourced from Will Styler's collection at: https://wstyler.ucsd.edu/puns/
- many subsequent puns were colleted from Jack Rhysider's unique endings in his podcast Darknet Diaries, made easily available by this Darknet Diaries Jokes repo.
- many other puns were sourced from the r/Physics subreddit question Anyone know any good Physics Jokes?
Notes on Blaming
- as far as possible and feasible, I attempt to blame the correct source for a pun, even if that person might not be the original, primary source for a pun but just the first trace that I found for it
- I attempt not to blame collectors, so while I credit Will Styler's collection, since he did not specifically take credit for creating a pun, he does not get blamed
- I changed many puns, sometimes substantially so, for various purposes: to fit the question-answer format; in an attempt to make them more humorous; to shorten their length; &tc.
- one of the most interesting ways in which I altered many puns was regarding gender. For historical reasons they were heavily biased with a male-centric view of the world, so I often changed it to a female-centric view, not because I think the world needs to "balance out" (and what would that take exactly?) but because it's interesting to see how many times I can surprise myself (and others) based on assumptions still; these "surprises" shed light on our implicit biases
- I have not attempted to reformulate them to include non-binary genders (yet)
- I did not blame myself in all these alterations, and since I altered most of them, in a way, you can blame me for it all
- I welcome any corrections, improvements, requests to include and exclude puns, better blaming, &tc.
- the point of origin for some of the app's code was Tim Ruscica's tutorials below, and Arpan Neupane's tutorial for authentication
- I also made regular use of chatGPT (free tier of https://chat.openai.com/) to help speed up and troubleshoot code and deployment
Tutorials from Tim Ruscica:
Tutorial from Arpan Neupane:
More Links:
To test locally before deployment, reproduce virtual env (see requirements.txt
) and issue:
source ~/usr/local/venv/py3/Scripts/activate
source local.env
python -u run.py
The local.env
contains export commands (for unix-style shell like Git Bash) used to set environment variables that will be available to any subprocesses or scripts executed from the shell, for example:
export BETA_USERS='<usernam_one>,<username_two>'
Deploying to a server is complex and varies depending on the infrastructure, but this is a typical test and deploy flow that I adhered to when making small changes to ensure they deployed correctly:
- commit changes
- copy changes into the
server/
folder to check diffs - revert changes that aren't needed in the (actual) server
- scp
server/
into (the actual) server - ssh into the server; modify user and permissions for files; restart server