Skip to content

Streamlit application - async

The streamlit_example.py script is a single-file web application that uses Streamlit and the OpenAIAsyncGenerator to generate courses.

A screenshot of a Streamlit application titled "OK Courses Course Generator." The interface allows users to enter the course topic, specify the number of lectures and subtopics per lecture, and configure options such as generating MP3 audio files and cover images for the course. A dropdown menu allows selecting a voice for the course lecturer, and an output directory path is specified. A button labeled "Generate outline" is shown, and a green notification box states, "Course outline generated and ready for review." Below, a course outline is displayed, including the title "Intelligence Explosion To ASI: A Hypothetical Play-By-Play" and the first lecture with subtopics like "Historical Development of AI Systems" and "Human vs Machine Learning Capabilities.

Run the app

To run the Streamlit app locally:

  1. Install uv.
  2. Set the OPENAI_API_KEY environment variable with your API key.
  3. Run uv sync.
  4. Launch the app with uv:

    uv run streamlit run examples/streamlit_example.py
    
  5. Navigate to the localhost URL shown in the command output.

    For example, in the following output, the URL is http://localhost:8501:

    [user@host okcourse]$ uv run streamlit run examples/streamlit_example.py
    
    You can now view your Streamlit app in your browser.
    
    Local URL: http://localhost:8501
    Network URL: http://192.168.0.5:8501
    
    2025-01-10 22:38:58 [INFO][streamlit] Initializing session state with new 'Course' instance...
    2025-01-10 22:38:58 [INFO][streamlit] Initializing session state with outline generation flag set to 'False'...
    2025-01-10 22:38:58 [INFO][streamlit] Initializing session state with course generation flag set to 'False'...
    

Streamlit example code listing

streamlit_example.py
"""OK Courses Course Generator

 Example Streamlit application that generates a course in four steps:

 1. Generate a course outline.
 2. Generate the course lectures based on the outline.
 3. (Optional) Generate a cover image based on the course title.
 4. (Optional) Generate TTS audio for the course; uses the cover image (if generated) for the MP3 album art tag.
"""
import asyncio
from pathlib import Path
import streamlit as st

from okcourse import Course, OpenAIAsyncGenerator
from okcourse.generators.openai.openai_utils import AIModels, get_usable_models_async, tts_voices
from okcourse.constants import MAX_LECTURES
from okcourse.prompt_library import PROMPT_COLLECTION
from okcourse.utils.log_utils import get_logger
from okcourse.utils.text_utils import get_duration_string_from_seconds


async def main():

    if "logger" not in st.session_state:
        st.session_state.logger = get_logger("streamlit")

    log = st.session_state.logger

    st.title("OK Courses Course Generator")

    # Initialize session state variables
    if "course" not in st.session_state:
        log.info("Initializing session state with new 'Course' instance...")
        st.session_state.course = Course()

    if "do_generate_outline" not in st.session_state:
        log.info("Initializing session state with outline generation flag set to 'False'...")
        st.session_state.do_generate_outline = False

    if "do_generate_course" not in st.session_state:
        log.info("Initializing session state with course generation flag set to 'False'...")
        st.session_state.do_generate_course = False

    # Flags to track when the user has accepted (or repeatedly regenerated) certain outputs
    if "lectures_done" not in st.session_state:
        st.session_state.lectures_done = False
    if "cover_image_done" not in st.session_state:
        st.session_state.cover_image_done = False

    course = st.session_state.course

    # Course style drop-down
    prompt_options = {prompt.description: prompt for prompt in PROMPT_COLLECTION}
    selected_prompt_name = st.selectbox("Course style", options=list(prompt_options.keys()))
    selected_prompt = prompt_options[selected_prompt_name]
    course.settings.prompts = selected_prompt

    # Course title text box
    course.title = st.text_input(
        "Course title", placeholder="Artificial Super Intelligence: Paperclips, Gray Goo, And You"
    )

    # AI model selection drop-downs
    usable_model_options: AIModels = await get_usable_models_async()
    course.settings.text_model_outline = st.selectbox(
        "Outline model",
        options=usable_model_options.text_models,
        placeholder="Choose an AI model for outline generation",
    )
    course.settings.text_model_lecture = st.selectbox(
        "Lecture model",
        options=usable_model_options.text_models,
        placeholder="Choose an AI model for lecture generation",
    )

    # Lecture and subtopic count checkboxes
    course.settings.num_lectures = st.number_input(
        "Number of lectures:", min_value=1, max_value=MAX_LECTURES, value=4, step=1
    )
    course.settings.num_subtopics = st.number_input(
        "Number of subtopics per lecture:", min_value=1, max_value=10, value=4, step=1
    )

    # Checkboxes for generating image/audio
    generate_image = st.checkbox("Generate course image (PNG)", value=False)
    generate_audio = st.checkbox("Generate course audio (MP3)", value=False)

    generator = OpenAIAsyncGenerator(course)

    if generate_audio:
        course.settings.tts_voice = st.selectbox("Choose a voice for the course lecturer", options=tts_voices)

    course.settings.output_directory = (
        Path(st.text_input("Output directory", value=course.settings.output_directory)).expanduser().resolve()
    )

    # Generate the outline
    if st.button("Generate outline") or st.session_state.do_generate_outline:
        if not course.title.strip():
            st.error("Enter a course title.")
        else:
            try:
                with st.spinner("Generating course outline..."):
                    st.session_state.do_generate_outline = False
                    course = await generator.generate_outline(course)
                    st.success("Course outline generated and ready for review.")
            except Exception as e:
                st.error(f"Failed to generate outline: {e}")
                log.error(f"Failed to generate outline: {e}")
                raise e

    # Display outline for review and allow regeneration
    if course.outline:
        st.write("## Course outline")
        st.write(str(course.outline))

        col_outline_regen, col_outline_ok = st.columns(2)
        if col_outline_regen.button("Regenerate outline"):
            course.outline = None
            st.session_state.do_generate_outline = True
            st.rerun()

        if col_outline_ok.button("Use this outline"):
            # Reset all acceptance flags and start the generation process
            st.session_state.do_generate_course = True
            st.session_state.lectures_done = False
            st.session_state.cover_image_done = False
            st.rerun()

    if st.session_state.do_generate_course and course.outline:
        # ---------------------
        # Step 1: Lectures
        # ---------------------
        if not st.session_state.lectures_done:
            # If no lectures exist, generate them
            if not course.lectures:
                try:
                    with st.spinner("Generating lectures..."):
                        course = await generator.generate_lectures(course)
                except Exception as e:
                    st.error(f"Failed to generate lectures: {e}")
                    log.error(f"Failed to generate lectures: {e}")
                    return

            # Display generated lectures
            st.write("## Lectures")
            for lecture in course.lectures:
                st.write(f"### Lecture {lecture.number}: {lecture.title}")
                st.write(lecture.text)

            col_lecture_regen, col_lecture_ok = st.columns(2)
            if col_lecture_regen.button("Regenerate lectures"):
                course.lectures = []
                st.rerun()
            if col_lecture_ok.button("Use these lectures"):
                st.session_state.lectures_done = True
                st.rerun()

        # ---------------------
        # Step 2: Cover Image
        # ---------------------
        if st.session_state.lectures_done and generate_image and not st.session_state.cover_image_done:
            # If no cover image has been generated, do so
            if not course.generation_info.image_file_path or not course.generation_info.image_file_path.exists():
                try:
                    with st.spinner("Generating cover image..."):
                        course = await generator.generate_image(course)
                except Exception as e:
                    st.error(f"Failed to generate course image: {e}")
                    log.error(f"Failed to generate course image: {e}")
                    return

            # Display generated cover image
            img_path = course.generation_info.image_file_path
            if img_path and img_path.exists():
                st.image(str(img_path), caption=course.title)

            img_col_left, img_col_right = st.columns(2)
            if img_col_left.button("Regenerate cover image"):
                if img_path and img_path.exists():
                    img_path.unlink(missing_ok=True)
                course.generation_info.image_file_path = None
                st.rerun()

            if img_col_right.button("Use this cover image"):
                st.session_state.cover_image_done = True
                st.rerun()

        # ---------------------
        # Step 3: Audio (if selected), then finalize
        # ---------------------
        # Only proceed to audio (and final summary) if either no cover image is requested or it is done
        if st.session_state.lectures_done and (not generate_image or st.session_state.cover_image_done):
            if generate_audio and (
                not course.generation_info.audio_file_path or not course.generation_info.audio_file_path.exists()
            ):
                try:
                    with st.spinner("Generating course audio..."):
                        course = await generator.generate_audio(course)
                except Exception as e:
                    st.error(f"Failed to generate course audio: {e}")
                    log.error(f"Failed to generate course audio: {e}")

            # If audio was generated, display it
            audio_path = course.generation_info.audio_file_path
            if generate_audio and audio_path and audio_path.exists():
                st.audio(str(audio_path), format="audio/mp3")

            # Final generation info
            total_time_seconds = (
                course.generation_info.outline_gen_elapsed_seconds
                + course.generation_info.lecture_gen_elapsed_seconds
                + course.generation_info.image_gen_elapsed_seconds
                + course.generation_info.audio_gen_elapsed_seconds
            )
            total_generation_time = get_duration_string_from_seconds(total_time_seconds)
            st.success(f"Course generated in {total_generation_time}.")
            st.write("## Generation details")
            st.json(course.generation_info.model_dump())

            # Reset flags to allow a fresh run if desired
            st.session_state.do_generate_course = False
            st.session_state.lectures_done = False
            st.session_state.cover_image_done = False


if __name__ == "__main__":
    asyncio.run(main())