Simplifying Blog Writing with OpenAI's ChatGPT API - Auto-generating Summaries, Tags, and Titles for Your Posts

As a writer, you know how important it is to create compelling content that engages your readers. But coming up with catchy titles, insightful summaries, and relevant tags can be time-consuming and challenging. I know I struggle a lot with this, no matter how well I know the topic I’m writing about.

Enter OpenAI’s ChatGPT API.

They recently (March 1, 2023) released the ChatGPT model that powers the web interface, gpt-3.5-turbo, and it’s now available for developers to use in their own applications. You can read more about it in their blog post, there are other exciting news there too.

In this blog post, I will walk you through a script that uses this new model to automate the process of generating summaries, tags, and titles for your blog posts.

And yes, a great deal of this post was generated by the ChatGPT model. Sue me.


Before we begin, you will need to have the following:

  • A OpenAI API key
  • Python 3.x installed on your system

If you haven’t already, you can sign up for OpenAI API access on their website.

Installing the Dependencies

pip install openai backoff python-frontmatter

The Script

import glob
import json
import sys
import termios
import tty

import backoff
import frontmatter
import openai

openai.api_key = "OPENAI_API_KEY"

def _getch():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
        ch =
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

@backoff.on_exception(backoff.expo, openai.error.RateLimitError)
def completions_with_backoff(**kwargs):
    return openai.ChatCompletion.create(**kwargs)

system_prompt = [
    "You are ChatGPT, a helpful assistant.",
    "You generate insightful short summaries and relevant tags for user supplied blog posts.",
    "Assume that the blog posts are in Markdown format.",
    "You don't generate summaries that are cryptic or unhelpful.",
    "You don't generate clickbait titles or summaries.",
    "The summary you generate is SEO friendly and no longer than 80 characters.",
    "Tags you generate are SEO friendly, lowercase and a single word each.",
    "You can reply with a title field that is different from the original if it's more SEO friendly.",
    "If the language is pt_BR, you must generate a summary and a title in Brazilian portuguese.",
    "The title field should be a string.",
    "The summary field should be a string.",
    "The tags field should be a list of strings.",
    "Don't include any kind of notes."
    "Your response should be in JSON format.",
    "Don't reply with anything other than the JSON object."

for fname in glob.glob("../content/english/post/*.md"):
    if fname.split('/')[-1] == "":

    with open(fname, "r") as f:
        post = frontmatter.loads(
        title = post['title']
        categories = post['categories']
        content = post.content
        summary = post.get('summary', '')
        tags = post.get('tags', [])
        generated = post.get('generated', False)
        language = post.get('language', 'en')
    print(f'{fname} ({language})')

    if generated:
        print("  skipping already generated post")

    user_prompt = [
        "Here's a blog post I'd like to summarize:",
        f"title: {title}",
        f"categories: {categories}",
        f"tags: {tags}",
        f"language: {language}",
        "content:", content]

    context_length = len('\n'.join(system_prompt + user_prompt)) * 0.75
    context_length_without_content = len('\n'.join(system_prompt + user_prompt[:-1])) * 0.75
    if context_length > 4096:
        print("  ! reducing context length...")
        diff = int(4096 - context_length_without_content)
        user_prompt[-1] = user_prompt[-1][:diff]

    while True:
        completion = completions_with_backoff(
                {"role": "system", "content": '\n'.join(system_prompt)},
                {"role": "user", "content": '\n'.join(user_prompt)},

        result = {}
            result = json.loads(completion["choices"][0]["message"]["content"].strip())
        except json.decoder.JSONDecodeError:
                fields = completion["choices"][0]["message"]["content"].strip().split('\n')
                for field in fields:
                    key = field.split(':')[0].strip()
                    value = ':'.join(field.split(':')[1:])
                    result[key] = value
            except Exception as e:
                print("  [-] Failed to parse response")

        new_title = result.get('title', title).strip()
        new_summary = result.get('summary', summary).strip()
        new_tags = result.get('tags', tags)

        print(f"  oldTitle: {title}")
        print(f"  newTitle: {new_title}")
        print(f"  oldTags: {tags}")
        print(f"  newTags: {new_tags}")
        print(f"  oldSummary: {summary}")
        print(f"  newSummary: {new_summary}")

        print("  accept? [y/n/s/q] ", end=' ')
        ch = _getch()

        if ch == 'y':
            post['title'] = new_title
            post['tags'] = new_tags
            post['summary'] = new_summary
            post['generated'] = True
            with open(fname, "w") as f:
            print('  saved...')
        elif ch == 'n':
            print('  retrying...')
        elif ch == 'q':
            print('  exiting...')
            print('  skipping...')

Example output

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (pt_BR)
  skipping already generated post

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (en)
  skipping already generated post

../content/english/post/ (en)
  ! reducing context length...

  oldTitle: SystemD Timers vs Code Loops
  newTitle: SystemD Timers vs Code Loops

  oldTags: ['timers', 'code', 'systemd']
  newTags: ['timers', 'code', 'systemd', 'linux', 'timerfd', 'resource accounting']

  oldSummary: Comparing the effectiveness of infinite loop strategies on Linux systems
  newSummary: Comparing SystemD Timers vs code loops effectiveness in computation
              and energy consumption on Linux, including the use of timerfd and 
              resource accounting

  accept? [y/n/s/q]


This script performs a task of generating summaries and relevant tags for user-supplied blog posts using OpenAI’s GPT-3 language model. The script reads the markdown files of blog posts from the ../content/english/post/ directory, extracts the necessary information like title, content, categories, tags, summary, and language from the file’s frontmatter, and generates a user prompt to get the summary and tags using GPT-3.

The script uses the openai Python package to access the GPT-3 API, which requires an API key to be set using openai.api_key. It also uses the frontmatter package to extract information from the markdown files' frontmatter and the glob package to read all markdown files in the specified directory.

The completions_with_backoff function uses the backoff package to retry the API call if it encounters a RateLimitError exception. It calls the openai.ChatCompletion.create() method to generate the completion using the GPT-3 language model specified by the model parameter and the user prompt specified by the messages parameter.

The script generates a system_prompt list containing the prompts for the language model. The user_prompt list is generated for each blog post and contains the information extracted from the post’s frontmatter and the language model prompts.

The script then calculates the context_length and context_length_without_content of the user prompt and checks if it exceeds the maximum length of 4096 characters allowed by the GPT-3 API. If the length exceeds the limit, it truncates the content field of the user prompt.

The script then enters a loop to generate a completion for the user prompt and prints the current state of the post, including the old and new title, tags, and summary generated by GPT-3. The user can then accept, reject, or skip the generated summary and tags by entering ‘y’, ‘n’, or ’s' respectively. Entering ‘q’ will exit the script.

If the user accepts the generated summary and tags, the script updates the post’s frontmatter with the new information and sets the generated field to True. Finally, it writes the updated frontmatter back to the file and moves on to the next post.


In the context of natural language processing and artificial intelligence, prompting refers to the process of providing input to a language model or AI system to generate a response or output. A prompt can be a question, a statement, or any other form of input that the language model or AI system can process to generate a relevant response. The quality of the prompt can have a significant impact on the quality and relevance of the output generated by the language model or AI system. Therefore, designing effective prompts is an essential part of developing natural language processing and AI systems that can effectively understand and respond to human language.

This prompt exists as a set of guidelines for OpenAI’s ChatGPT language model to ensure that it generates insightful summaries and relevant tags for user-supplied blog posts in a consistent and user-friendly manner. The prompt outlines the requirements for the length and format of the summary and tags and ensures that they are SEO-friendly and easy to understand. Additionally, the prompt specifies that the language model should not generate clickbait titles or summaries and that it should avoid generating cryptic or unhelpful content. Finally, the prompt provides guidelines for generating responses in different languages and ensuring that the output is formatted correctly.

I also try my best to have the output be as consistent as possible. Language models are not deterministic, so the output can vary slightly each time you run the script. Getting a valid JSON most of the time is already a win.


In conclusion, the potential of language models like GPT-3 is vast and far-reaching, and helping bloggers is just one of the many possibilities they offer.

From healthcare to finance, legal services to customer service, and beyond, these models can transform industries by automating tasks, improving efficiency, and enhancing the way we live and work.

Their ability to understand and generate human-like language has opened up a world of new possibilities for developing innovative solutions that can make our lives easier and more convenient.

As research in this area continues to evolve, we can expect to see even more exciting applications of language models that can shape the future of technology and how we interact with it.

As these language models continue to advance and become more powerful, it raises important questions about the potential impact on information security.

With the ability to generate convincing and sophisticated language, there is a risk (a concrete one, already seem in the wild) of these models being used for malicious purposes, such as generating convincing phishing emails, fake news, or even deepfakes.

As a society, we need to be mindful of these risks and take proactive steps to address them.

This might include developing new strategies for detecting and preventing the misuse of language models, investing in more advanced security measures, and educating users on how to identify and protect themselves against potential threats.

Ultimately, it is up to all of us to ensure that these powerful tools are used responsibly and ethically to advance our collective interests and improve our lives.

comments powered by Disqus