medium_com
medium_com
-- 2
Intro
I’ve been making projects for a while on YouTube. In August 2023, I
wanted to make a bigger project. By ‘bigger’ I mean over 1 year.
So, I started ideating with my friend Jeremy. We had a couple ideas, one
was a new ticketing system for fans and another was a personal AI agent to
extract purchasable products from content you watch.
These seemed a bit too big scoped for me and my partner, so we decided to
make a platform to make AI-Generated E-Books, after seeing a rise in
people making money by making and selling these types of E-Books online.
import uuid
import openai
import os
import requests
class GptWrapper:
def __init__(self):
self.OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.OPENAI_API_KEY = self.OPENAI_API_KEY
self.CHAT_GPT_MODELS = [
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo",
]
self.GPT_MODEL_INDEX = 0
self.CHAT_GPT_MODEL = self.CHAT_GPT_MODELS[self.GPT_MODEL_INDEX]
self.convos = dict()
def retry(func):
def wrapper(self, *args, **kwargs):
while self.GPT_MODEL_INDEX < len(self.CHAT_GPT_MODELS) - 1:
try:
result = func(self, *args, **kwargs)
return result
except Exception as e:
# We should make this specific to the model error code
print(f"An exception occurred: {e}")
print(
f"Switching to the next model in self.CHAT_GPT_MODELS"
)
self.GPT_MODEL_INDEX += 1
self.CHAT_GPT_MODEL = self.CHAT_GPT_MODELS[
self.GPT_MODEL_INDEX
]
else:
raise Exception(
f"All models in self.CHAT_GPT_MODELS have been tried and"
f" exceeded"
)
return wrapper
@retry
def start_convo(self, system):
convo_id = str(uuid.uuid4())
self.convos[convo_id] = [
{"role": "system", "content": system},
]
return convo_id
@retry
def msg_in_convo(self, convo_id, prompt):
self.convos[convo_id].append({"role": "user", "content": prompt})
response = openai.ChatCompletion.create(
model=self.CHAT_GPT_MODEL,
messages=self.convos[convo_id],
)
text = response.choices[0].message.content.strip()
self.convos[convo_id].append({"role": "assistant", "content": text})
return text
@retry
def msg_in_convo_given_history(self, convo_id, messages):
self.convos[convo_id] = messages
response = openai.ChatCompletion.create(
model=self.CHAT_GPT_MODEL,
messages=messages,
)
text = response.choices[0].message.content.strip()
self.convos[convo_id].append({"role": "assistant", "content": text})
return text
@retry
def ask_question_in_convo(self, convo_id, question):
question += "Answer with only a single word: 'True' or 'False'"
self.convos[convo_id].append({"role": "user", "content": question})
response = openai.ChatCompletion.create(
model=self.CHAT_GPT_MODEL,
messages=self.convos[convo_id],
)
text = response.choices[0].message.content.strip()
response = True
if text.lower() == "true":
response = True
elif text.lower() == "false":
response = False
else:
raise Exception("Response is not a bool")
self.convos[convo_id].append({"role": "assistant", "content": text})
return response
@retry
def generate_photo(self, photo_prompt):
improved_gpt_prompt = (
f"A positive image of: {photo_prompt}, rendered artistically in a"
" chic, cartooney, minimalistic style."
)
response = openai.Image.create(
model="dall-e-3",
prompt=improved_gpt_prompt,
size="1024x1024",
quality="standard",
n=1,
)
image_url = response.data[0].url
img_data = requests.get(image_url).content
return img_data
1. Generate Title
5. Format it as a Docx
6. Create A Cover
7. Turn it in to a PDF
Let’s Do It:
Generate Title
def generate_title(
self,
topic,
target_audience,
):
convo_id = self.GPT_WRAPPER.start_convo(GptSystems.AUTHOR_SYSTEM)
# Generate title
title_prompt = (
f'We are writing an eBook. It is about "{topic}". Our'
f' reader is: "{target_audience}". Write a short, catch'
" title clearly directed at our reader that is less than"
" 9 words and proposes a “big promise” that will be sure to grab"
" the readers attention."
)
title = self.GPT_WRAPPER.msg_in_convo(convo_id, title_prompt)
# remove surrounding quotes from title (if any)
title = title.replace('"', "")
return title
Generate Outline for the Book / Save Outline As A Data Structure (Map)
"""
Verify that ChatGPT is producing an outline in appropriate
"""
@retry(max_retries=3)
def generate_outline(
self,
topic,
target_audience,
title,
num_chapters,
num_subsections,
):
convo_id = self.GPT_WRAPPER.start_convo(GptSystems.AUTHOR_SYSTEM)
outline_prompt = (
f'We are writing an eBook called "{title}". It is about'
f' "{topic}". Our reader is: "{target_audience}". Create'
" a compehensive outline for our ebook, which will have"
f" {num_chapters} chapter(s). Each chapter should have exactly"
f" {num_subsections} subsection(s) Output Format for prompt:"
" python dict with key: chapter title, value: a single list/array"
" containing subsection titles within the chapter (the subtopics"
" should be inside the list). The chapter titles should be"
' prepended with the chapter number, like this: "Chapter 5:'
' [chapter title]". The subsection titles should be prepended'
' with the {chapter number.subtitle number}, like this: "5.4:'
' [subsection title]". '
)
outline_json = self.GPT_WRAPPER.msg_in_convo(convo_id, outline_prompt)
outline_json = outline_json[
outline_json.find("{") : outline_json.rfind("}") + 1
]
outline = json.loads(outline_json)
if not self.verify_outline(outline, num_chapters, num_subsections):
raise Exception("Outline not well formed!")
return outline
...
outline = json.loads(outline_json)
@retry(max_retries=1)
def generate_chapter_content(
self, title, topic, target_audience, idx, chapter, subtopic
):
convo_id = self.GPT_WRAPPER.start_convo(GptSystems.AUTHOR_SYSTEM)
num_words_str = "500 to 700"
content_prompt = (
f'We are writing an eBook called "{title}". Overall, it is about'
f' "{topic}". Our reader is: "{target_audience}". We are'
f" currently writing the #{idx+1} section for the chapter:"
f' "{chapter}". Using at least {num_words_str} words, write the'
" full contents of the section regarding this subtopic:"
f' "{subtopic}". The output should be as helpful to the reader as'
" possible. Include quantitative facts and statistics, with"
" references. Go as in depth as necessary. You can split this"
" into multiple paragraphs if you see fit. The output should also"
' be in cohesive paragraph form. Do not include any "[Insert'
' ___]" parts that will require manual editing in the book later.'
" If find yourself needing to put 'insert [blank]' anywhere, do"
" not do it (this is very important). If you do not know"
" something, do not include it in the output. Exclude any"
" auxillary information like the word count, as the entire"
" output will go directly into the ebook for readers, without any"
" human procesing. Remember the {num_words_str} word minimum,"
" please adhere to it."
)
content = self.GPT_WRAPPER.msg_in_convo(convo_id, content_prompt)
# Search for the pattern in the first 100 characters of the input string
match = re.search(pattern, content[:search_limit])
if match:
# Find the index of the next newline character after the match
newline_index = content.find('\n', match.end())
if newline_index != -1:
# Extract the substring after the newline character
result_string = content[newline_index + 1:]
content = content.strip()
return result_string
return content
try:
content = content.strip()
content = remove_subtopic_from_content(content, subtopic)
except:
pass
print(content)
return content
Format it as a Docx
We can use the Python library docx.
@retry(max_retries=1)
def generate_docx(
self,
topic,
target_audience,
title,
outline,
docx_file,
book_template,
preview,
actionable_steps=False,
):
document = Document(book_template)
document.add_page_break() # Lets add a page and then remove it later. This
document.add_heading("Table of Contents")
for chapter, subtopics in outline.items():
document.add_heading(chapter, level=2)
for idx, subtopic in enumerate(subtopics):
document.add_heading("\t" + subtopic, level=3)
document.add_page_break()
chapter_num = 1
for chapter, subtopics in outline.items():
document.add_heading(chapter, level=1)
subtopics_content = []
document.add_heading(subtopic, level=2)
content = self.generate_chapter_content(
title, topic, target_audience, idx, chapter, subtopic
)
document.add_paragraph(content)
subtopics_content.append(content)
document.save(docx_file)
def generate_cover(
self, cover_template, title, topic, target_audience, output_file, preview
):
doc = DocxTemplate(cover_template)
if preview:
self.cover_photo_location = f"app/ai_book_generation/templates/covers/pr
else:
self.generate_cover_photo(
title, topic, target_audience, self.cover_photo_location
)
imagen = InlineImage(
doc, self.cover_photo_location, width=Mm(120)
) # width is in millimetres
context = {"title": title, "subtext": "NJ Publishing", "image": imagen}
doc.render(context)
doc.save(self.cover_location)
convert(self.cover_location, output_file)
Turn it in to a PDF
Time to use Python library docx2pdf
convert(docx_file, self.content_pdf_location_temp)
So let’s make this into a SAAS (Software As A Service), so people who want
to make ebooks can do so on our platform.
We made a frontend for this using Next.js, which I won’t get into because I
don’t really do frontend stuff.
Then, we have code that we can run locally on our computer, but now we
want to run it on the Internet. To do this, we have to turn our backend
code into an API (Application Programming Interface).
There’s a ton of ways to do this (Django, Flask, FastAPI), but I think the
simplest and easist to use is Flask, so that’s what we’re going to go with.
Surprisingly, I was not able to find any templates that would help us with
this. I thought this was a pretty common use case. So, I made my own —
it’s here: https://github.com/nathan-149/flask-backend-api-
template/blob/main/README.md
The frontend simply then calls this new API with HTTP calls, and our
backend code runs as if it did locally!
Payment Flow
Parallelization Of Work
I also had an existential crisis that I didn’t want to cater only towards
these ‘hustlers’ so we pivoted to catering to people who wanted to actually
learn something. These E-Books were pretty good for this, and I generated
books about the History of The Rothschild Family and Quantum
Computing and they were actually really great.
Home Page
Applying To YC
I applied to YC for this but ultimately got rejected. The Total Addressable
Market in the Public Education sector was not looking very great, so I
don’t blame them. All good though — I’ll try again for a startup (not just to
YC) with a different, better idea. :)
Conclusion
I’ll probably add to this at some point, but I hope you liked reading this.
Let me know!
-- 2
Responses (2)
Help Status About Careers Press Blog Privacy Terms Text to speech Teams