Welcome to part 4. This is where we put it all together and finally create our fully-fledged meme generator. We’ll be creating the frontend for our project using Streamlit, but before we can do so, we need to make some final updates to our get_meme.py
code.
So open back up get_meme.py
one last time and let’s start with the imports:
import json import os from pathlib import Path # new import from typing import TypedDict from dotenv import load_dotenv from openai import OpenAI from load_meme_data import MemeData, load_meme_data, load_meme_data_flat_string # updated import from meme_image_editor import overlay_text_on_image # new import from system_instructions import get_system_instructions
We’ve added an import for Path
from pathlib
, which we’ve used several times before. We’ve added the MemeData
datatype and load_meme_data
imports from load_meme_data.py
, for both the datatype and loading the meme data as an object instead of a flat string. Last but not least, we’ve added an import for our overlay_text_on_image
function we worked so hard on in the last part.
I’ll provide this for clarity, there are zero changes in the following code:
## No changes here ## class MemeGPTOutput(TypedDict): meme_id: int meme_name: str meme_text: list[str] load_dotenv() CLIENT = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) MEME_DATA_TEXT = load_meme_data_flat_string() SYSTEM_MESSAGE = { "role": "system", "content": [ { "type": "text", "text": get_system_instructions(MEME_DATA_TEXT), } ], } def call_chatgpt(user_message): response = CLIENT.chat.completions.create( model="gpt-4o", messages=[SYSTEM_MESSAGE, user_message], # type: ignore temperature=1, max_tokens=2048, response_format={"type": "json_object"}, ) return response.choices[0].message.content
So that all stays the same. The only function we’re going to be updating is the next one, generate_memes
. Update the function like this:
def generate_memes(user_input: str) -> list[str] | None: user_message = { "role": "user", "content": [ { "type": "text", "text": user_input, } ], } response = call_chatgpt(user_message) print(response) # Added a print statement for the terminal if not response: print("No response from the model, something went wrong.") return try: meme_output: list[MemeGPTOutput] = json.loads(response)['output'] except json.JSONDecodeError: print("Invalid response from the model.") return ## New code starts here ## meme_data: list[MemeData] = load_meme_data() images = [] for ai_meme in meme_output: chosen_meme_index = int(ai_meme["meme_id"]) print(f"MemeGPT chose the meme: {meme_data[chosen_meme_index]['name']}") print(f"Meme text: {ai_meme['meme_text']}") image_path: Path = overlay_text_on_image(meme_data[chosen_meme_index], ai_meme["meme_text"]) images.append(str(image_path)) return images
As you can see I’ve added a print statement to print the response
from ChatGPT to the terminal. This is useful for debugging purposes. Then we added a whole new block of code at the end. The first thing we do is load the meme data as a list
of MemeData
objects using our load_meme_data()
function, saving it to the variable meme_data
.
We then create an empty list called images
to store the paths of the images we generate. We loop through each ai_meme
in the meme_output
list, first getting the index of the meme chosen by ChatGPT. We then print the name of the meme and the text generated by ChatGPT to the terminal, as this could be helpful to see in real-time for debugging purposes.
Now we simply call our overlay_text_on_image
function, passing in the chosen_meme_index
of the meme_data
list and the ai_meme
‘s meme_text
. This function will return the path of the image it creates, which we then append to the images
list. Note that we convert the Path
object to a string using str()
before appending it to the list, as I happen to know that Streamlit wants paths in this string format.
Finally, we return the images
list. This function will now return a list of paths to the 3 meme images, which we can then use to display them in our Streamlit frontend.
Testing the code
You can test-run this file. The code in the if __name__ == "__main__":
block requires no changes for this:
## No changes here ## if __name__ == "__main__": print("Welcome to the meme generator!") print("You can provide a situation or a topic to generate a meme.") generate_memes(user_input=input("Please provide a topic or situation: "))
So try it out and give it a topic. Here is my output:
Welcome to the meme generator! You can provide a situation or a topic to generate a meme. Please provide a topic or situation: I am using AI to generate unlimited memes. { "output": [ { "meme_id": 10, "meme_name": "Roll Safe Think About It", "meme_text": [ "Never run out of memes", "If you use AI to generate them" ] }, { "meme_id": 5, "meme_name": "Expanding Brain", "meme_text": [ "Browsing meme websites", "Creating memes manually", "Using meme generators", "Using AI to generate unlimited memes" ] }, { "meme_id": 2, "meme_name": "Left Exit 12 Off Ramp", "meme_text": [ "Searching for trending memes", "Me", "Using AI to make memes" ] } ] } MemeGPT chose the meme: Roll Safe Think About It Meme text: ['Never run out of memes', 'If you use AI to generate them'] Image saved to C:\Coding_Vault\Meme_Gen_Draft\output\23e36ce1-b791-42c1-a852-67cac4f62f42.png MemeGPT chose the meme: Expanding Brain Meme text: ['Browsing meme websites', 'Creating memes manually', 'Using meme generators', 'Using AI to generate unlimited memes'] Image saved to C:\Coding_Vault\Meme_Gen_Draft\output\69c20322-80f7-4184-ae5d-18771b34de00.png MemeGPT chose the meme: Left Exit 12 Off Ramp Meme text: ['Searching for trending memes', 'Me', 'Using AI to make memes'] Image saved to C:\Coding_Vault\Meme_Gen_Draft\output\21da4834-ceed-493d-801c-cef7b7fb69a7.png
And here are my three images:
Alright! All looks good and ready to go.
Streamlit frontend
Now let’s get working on a quick frontend to make this a lot more comfortable to actually use. We’ll be using Streamlit for this, which is a quick and easy way to create web apps in Python. If you don’t have Streamlit installed, you can install it using pip:
pip install streamlit
Next up create a new file in your project’s main directory. I’m going to name mine app.py
:
π Meme_Gen π fonts π ARIAL.TTF π ComicSansMS3.ttf π impact.ttf π output π templates πΌ (all the images in here...) π .env π app.py --> create this file π get_meme.py π load_meme_data.py π meme_data.json π meme_image_editor.py π system_instructions.py
So open up your app.py
file and let’s start with our imports:
import streamlit as st from get_meme import generate_memes
We import streamlit
as st
so that we have a short alias to use. We then import our generate_memes
function from get_meme.py
.
Streamlit state
We’ll start by creating some session_state
to store the generated meme images:
if "images" not in st.session_state: st.session_state.images = []
The way this works is that Streamlit has a session_state
object that you can use to store data across different parts of your app. We’re checking if the key "images"
is not in the session_state
, and if it’s not, we’re creating it as an empty list. This way the state will be initialized to an empty list when the code runs the first time.
The reason we wrote the code like this is that Streamlit reruns the entire script every time you interact with the app. This means that if we didn’t check if the key was already in the session_state
, it would reset the images list back to an empty list every time you interacted with the app.
So the way we wrote it now, the second time the script runs, the key "images"
will already be in the session_state
, and it will not be overwritten with an empty list, thus preserving our values between runs. Let’s add one more of these state variables for the loading state where it is currently generating memes, as we want to disable the button during this process:
if 'run_button' in st.session_state and st.session_state.run_button == True: st.session_state.running = True else: st.session_state.running = False
This one is a bit more complicated as you can see we have two states in here. The first state is run_button
and the second state is running
. Later in our code, we are going to create the button the user presses to generate memes, and we’re going to give this button a key
value of run_button
. What this means is that when the button is pressed, Streamlit will automatically set a run_button
key in the session_state
to True
.
When this session_state.run_button
is True
, so the user has pressed the button, we’re going to set a new state variable called running
to True
. Remember the important point about the whole code being rerun whenever there is an update in the app. This means that whenever there is an app update but it has not been triggered by the user clicking the button, the run_button
key will not be set to True
, and the running
key will be set to False
.
This may look a bit complicated, but dealing with state in Streamlit can be a bit tricky. The point is that under all conditions we now have a running
key in the session_state
that we can use to check if the app is currently generating memes or not, provided we don’t forget to create a button with the key run_button
of course. This will be useful later when we want to disable the button while the app is generating memes.
That was honestly the most confusing part of the Streamlit app code and the rest is very straightforward. If this seems a bit confusing, just carry on with me and when we finish up the whole code read over it again, keeping in mind the whole code is rerun every time there is an update in the app, and it should make more sense.
Page setup and header
Now let’s continue on below the state:
st.set_page_config( page_title="Meme Generator", page_icon="π§", layout="wide", )
Streamlit, which we have aliased as st
has a set_page_config
function that we can use to set some configurations for our app. We’re setting the page_title
to “Meme Generator”, the page_icon
to a wizard emoji, and the layout
to “wide”. This will make our app look a bit nicer.
The next two Streamlit functions are also fairly self-explanatory:
st.title("Welcome to the Meme Generator! π§") st.write( "##### Please provide a situation or a topic to generate a meme. You can provide a short topic or paste in a whole story or article." )
The .title
function creates a large title text for us, displaying whatever string we pass in, and the .write
function does essentially the same except for a normal text block. The #####
in the string passed to .write
is a markdown syntax for a smaller header text and will make the text a bit bigger than standard text, but much smaller than the title text.
Note that you can also pass in any other markdown syntax you want to use here, like **bold**
or *italic*
, which would be displayed as bold and italic respectively.
The next thing we need is a text input field for the user to input their topic or situation:
user_input = st.text_area("Situation, topic or article:", height=200)
Again, pretty self-explanatory but the .text_area
does exactly what the name suggests, it creates a multi-line text area where the user can input text. We’re passing in the string “Situation, topic or article:” as the label for the text area, and we’re setting the height
to 200
pixels.
This will make the text area a bit larger than the default size, as I want the user to be able to type simple things themselves, but also copy-paste stuff like a news article or a story in there if they want to generate memes on current events without having to type it all out themselves.
So let’s see what we have so far. Go to your terminal window, making sure your terminal is in the project folder, and type:
streamlit run app.py
This will start the Streamlit app and open it in your default web browser. You should see something like this:
Not bad! Of course we do need to add some more stuff to get it to work.
Adding the button logic
Let’s first add the button to generate actual memes:
if st.button("Generate Memes", disabled=st.session_state.running, key='run_button'): print("Generating memes...") new_images = generate_memes(user_input) if new_images: st.session_state.images = new_images + st.session_state.images st.rerun()
Now the syntax with the if
statement here seems very odd so let me explain. In Streamlit, a button is declared using an if
statement because the st.button
function returns True
when the button is clicked. This allows us to execute the indented code following the button if
statement only when the button is clicked.
We declare the button with the label "Generate Memes"
, and we set the disabled
parameter to st.session_state.running
. This means that the button will be disabled when the running
key in the session_state
is True
. Now we give this button a key
of run_button
. This key
is the unique identifier for the button widget, so you cannot use the same key twice for different things.
So if we look back at the state we declared earlier in our file, when the button is pressed it will return True
as we have just learned. This sets the session_state.run_button
to True
, as we’re using a key
of run_button
. This will then set the session_state.running
to True
as well, which will in turn disable the button as the disabled
parameter reads this value.
Now for the actual indented code block that will run when the button is pressed. We print “Generating memes⦔ to the terminal, and then we call our generate_memes
function, passing in the user_input
text. We save the result to a variable called new_images
, and then we check if new_images
is not None
(i.e. the function ran properly and returned some images).
When we have our new images we’re going to set the session_state.images
to the new images plus the old images. This is because we want to keep the old images in the list as well, so we can display all the images generated in the app. Finally, we call st.rerun()
, which runs the code again to update the page for us with the new state. We don’t have anywhere to display the images yet, but when we do, this rerun()
will cause the new images to be rendered on the page.
Adding columns and displaying images
Now that we have a list with images in the session_state
, we need to display them on the actual page. I’m going to use four columns to do this to make it look extra fancy:
num_columns = 4 columns = st.columns(num_columns) for index, image_path in enumerate(st.session_state.images): with columns[index % num_columns]: st.image(str(image_path), use_container_width=True)
This may look confusing, so let’s go over it. We set the num_columns
to 4. This means that the images will be displayed in a grid with 4 columns. The st.columns
function creates a list of column objects. Each column object represents a column in the layout, 4 in this case.
In the next line, we loop through the list of image paths stored in st.session_state.images
. enumerate
is used to get both the index and the image path for each iteration, so, for instance, the first iteration would give us index = 0
and image_path = "path/to/first_image.png"
, and so on for each image with the index increasing by 1 each time.
The line with columns[index % num_columns]:
uses the modulo %
operator to get the remainder of the division of the index
by the number of columns
and ensures that the images are distributed evenly across the columns.
For example, if the index is 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, the result of index
%
num_columns
will be 0
, 1
, 2
, 3
, 0
, 1
, 2
, 3
, respectively. This means that the first image goes into the first column, the second image into the second column, and so on. After the fourth image, the fifth image goes back into the first column, creating a wrapping effect.
Finally, we use the st.image
function to display the image. We pass in the image_path
converted to a string by calling the str()
method, and we set use_container_width
to True
to make the image take up the full width of the column it’s in, making it look nicer.
Initial test run
We’re almost done! But let’s see if this works first. Go back to your terminal and run the Streamlit app again:
streamlit run app.py
Or you can just refresh the page if you still have it open. By the way, you can shut down the Streamlit app by pressing Ctrl + C
in the terminal window where you started it. With your Streamlit app running, give it a topic and press generate!
As you can see, you can just keep generating memes and it will fill them into a nice grid format for you, adding the new memes at the front of the list. Awesome! You can now generate memes to your heart’s content. Before I congratulate you on finishing the tutorial, I want to go over one more thing though. There are many ways in which this can still be improved, but let’s do one together to get you started, and I’ll leave the rest up to you.
Adding a delete button
When generating memes, not every meme will always be a hit. Sometimes the AI will generate something that is not that funny. So let’s add a delete button that will allow the user to get rid of the memes they don’t like. We’ll add a button to each image that will allow the user to delete that specific image.
First of all, we need a function that will delete an image from the session_state.images
list. Add this function straight after the st.session_state
state variables at the top of your app.py
file:
if "images" not in st.session_state: st.session_state.images = [] if 'run_button' in st.session_state and st.session_state.run_button == True: st.session_state.running = True else: st.session_state.running = False ## New code starts here ## def delete_image(image_path): st.session_state.images = [ img for img in st.session_state.images if img != image_path ]
We added a function named delete_image
that takes an image_path
as an argument. We then use a list comprehension that will create a new list with every img
inside of st.session_stage.images
, as long as the img
is not equal to !=
the image_path
we passed in. This effectively removes only that one image from the list. The result is then set as the new value of st.session_state.images
.
Now we need a new button to trigger this. Scroll back down to the column code all the way at the bottom. We’re going to add a button to each image inside of the loop here:
num_columns = 4 columns = st.columns(num_columns) for index, image_path in enumerate(st.session_state.images): with columns[index % num_columns]: st.image(str(image_path), use_container_width=True) ## New code starts here ## if st.button( "ποΈ Delete", key=f"delete_{index}", on_click=lambda image=image_path: delete_image(image), ): print("Deleting image...")
Below each image, we add a new button with the label “ποΈ Delete”. We give this button a key
of f"delete_{index}"
, which makes sure that every button created by this loop has a unique index, which is a requirement to make them work properly. The on_click
line is where we define what function will run when the user clicks the button.
The reason we’re not using an indented block of code below the if st.button
statement this time is that we need the code to run before the page gets updated again, which requires us to use the on_click
parameter. We’re using a lambda
function here which takes the image_path
as an argument and then calls the delete_image
function with this argument.
In case you’re not familiar with lambda functions the line:
## Example code ## lambda image_path: delete_image(image_path)
Is basically equivalent to:
## Example code ## def nameless_function(image_path): delete_image(image_path)
So you might think, why don’t we just pass in on_click=delete_image(image_path)
? The reason is that using the () will call the function directly when initializing the button. Just placing the button on the page without anyone having clicked it yet would have already deleted any images. So we use the lambda
function to delay the execution of the delete_image
function until the button is actually clicked.
Then finally inside of the if st.button
statement we print “Deleting image⦔ to the terminal just for debugging purposes. Now run the Streamlit app again and try it out!
So go ahead and generate a bunch of memes on anything:
Now let’s say we want to delete the second one as it’s pretty similar to number 5 and we don’t need it. Try pressing the delete button and it will instantly disappear!
There you go! A fully functional meme generator. It’s pretty affordable too as we only use a single ChatGPT call per 3 memes, and all the image editing magic is done locally on our own machine. Congratulations on making it this far. Play around with it, have fun, and make any edits you want to make it your own. You can add more features, fonts, your own favorite meme templates, etc.
I hope you thoroughly enjoyed this course and learned some new things along the way. We have many other cool courses available on the Finxter Academy, so feel free to check them out. As always, it has been my honor and pleasure to take this journey together with you, and until next time, happy coding!