import shutil import statistics import tempfile import uuid from dataclasses import dataclass, field from pathlib import Path from typing import List import streamlit as st from llama_index.core.schema import Document from streamlit.runtime.uploaded_file_manager import UploadedFile from aistorybooks.phidataa.classic_stories import PhiStoryBookGenerator @dataclass class AppInputs: """ Data class to hold the input values for the Streamlit app. """ uploaded_file: UploadedFile | None = None language: str = "German" level: str = "B1 Intermediate" summary_size: str = "Long (150 sentences/1200 words)" writing_style: str = "Philosophical" chunk_size: int = 10 padding: int = 1 skip_first_n_pages: int = 0 language_options: List[str] = field( default_factory=lambda: ["German", "English", "Spanish", "French"] ) level_options: List[str] = field( default_factory=lambda: [ "A1 Beginner", "A2 Elementary", "B1 Intermediate", "B2 Upper Intermediate", "C1 Advanced", "C2 Proficiency", ] ) summary_size_options: List[str] = field( default_factory=lambda: [ "Short (50 sentences/400 words)", "Medium (100 sentences/800 words)", "Long (150 sentences/1200 words)", ] ) writing_style_options: List[str] = field( default_factory=lambda: [ "Philosophical", "Narrative", "Descriptive", "Humorous", "Formal", ] ) def st_sidebar(inputs: AppInputs): """ Creates the sidebar for the Streamlit app and populates the input values. Args: inputs (AppInputs): An instance of the AppInputs data class. """ st.header("Input Options:") with st.form(key='inputs_form', border=False): inputs.uploaded_file = st.file_uploader( "Upload your novel (PDF)", type=["pdf"], accept_multiple_files=False, help="Upload the PDF file of the novel you want to convert.", ) inputs.language = st.selectbox( "Select Target Story Language", inputs.language_options, index=inputs.language_options.index(inputs.language), help="Choose the language you want the storybook to be in.", ) inputs.level = st.selectbox( "Select Language Level", inputs.level_options, index=inputs.level_options.index(inputs.level), help="Select the target language proficiency level for the storybook.", ) inputs.summary_size = st.selectbox( "Desired Summary Length (Per Chunk)", inputs.summary_size_options, index=inputs.summary_size_options.index(inputs.summary_size), help="Specify the desired length of the summary for each chunk of the novel.", ) inputs.writing_style = st.selectbox( "Desired Writing Style", inputs.writing_style_options, index=inputs.writing_style_options.index(inputs.writing_style), help="Choose the writing style for the generated storybook.", ) inputs.chunk_size = st.number_input( "Chunk Size", min_value=1, value=inputs.chunk_size, help="Number of pages to process per iteration. Larger chunks may take longer to process.", ) inputs.padding = st.number_input( "Padding", min_value=0, value=inputs.padding, help="Number of pages to overlap between chunks. Helps maintain context.", ) inputs.skip_first_n_pages = st.number_input( "Skip First N Pages", min_value=0, value=inputs.skip_first_n_pages, help="Number of pages to skip at the beginning of the novel (e.g., table of contents).", ) submit_button = st.form_submit_button(label='Submit') return submit_button def st_process_file(inputs: AppInputs) -> List[List[Document]]: uploaded_file_name = inputs.uploaded_file.name st.info(f"Uploaded File: **{inputs.uploaded_file.name}**. Preparing your storybook... (Working in the background)." f" \nPlease note: Processing is powered by the free tier of Gemini, which may experience rate limiting.", icon=":material/info:") try: temp_folder = Path(tempfile.mkdtemp(prefix="story_gen_temp_")) progress_value = 0 progress = st.progress(value=progress_value, text=f"Processing file...") pdf_file = temp_folder.joinpath(uploaded_file_name) md_file_name = f"{pdf_file.stem}.md" # pdf_file_final = pdf_file.parent.joinpath(f"{pdf_file.stem}_story.pdf") pdf_file.write_bytes(inputs.uploaded_file.getvalue()) generator = PhiStoryBookGenerator( language=inputs.language, level=inputs.level, summary_size=inputs.summary_size, writing_style=inputs.writing_style, ) st.session_state[md_file_name] = "" button_container = st.empty() info_container = st.empty() it = generator.run(pdf_file=pdf_file, chunk_size=inputs.chunk_size, padding=inputs.padding, skip_first_n_pages=inputs.skip_first_n_pages ) for response in it: if response.event == "RunFailed": st.error(f"{response.content}", icon=":material/error:") progress.progress(value=progress_value, text=f"Error: {response.content}") else: progress_value = response.metrics['progress_percent'] progress.progress(value=progress_value, text=response.metrics['progress_info']) st.session_state[md_file_name] += f"\n\n{response.content}" button_container.empty() button_container.download_button(label='Download Storybook as Markdown', data=st.session_state.get(md_file_name), file_name=md_file_name, mime='text/markdown', on_click="ignore", key=str(uuid.uuid4()), type="primary", icon=":material/download:", ) info_container.empty() metrics = generator.model.metrics avg_response_time = statistics.mean(metrics.get('response_times', [])) if metrics else 0 input_tokens = metrics.get('input_tokens', 0) if metrics else 0 output_tokens = metrics.get('output_tokens', 0) if metrics else 0 total_tokens = metrics.get('total_tokens', 0) if metrics else 0 info_container.info(f"Model: {generator.model.name} " f" \n Avg response time: {avg_response_time} " f" \n Input tokens: {input_tokens} " f" \n Output tokens: {output_tokens} " f" \n Total tokens: {total_tokens}", icon=":material/info:") finally: if temp_folder and temp_folder.exists(): shutil.rmtree(temp_folder) st.info(f"Temporary folder and its contents cleared", icon=":material/info:") def st_main_page(inputs: AppInputs): """ Creates the main page for the Streamlit app and displays the input values. Args: inputs (AppInputs): An instance of the AppInputs data class. """ st.title("Novel to Storybook Generator") st.write("---") options_text = f""" **Selected Options:** **Language:** {inputs.language} | **Language Level:** {inputs.level} | **Summary Length:** {inputs.summary_size} | **Writing Style:** {inputs.writing_style} | **Chunk Size:** {inputs.chunk_size} | **Padding:** {inputs.padding} | **Skip First N Pages:** {inputs.skip_first_n_pages} """ st.markdown(options_text) if inputs.uploaded_file: st_process_file(inputs=inputs) def st_set_css_and_footer(): st.markdown( """ """, unsafe_allow_html=True, ) footer_html = """
""" st.markdown(footer_html, unsafe_allow_html=True) def main(): st.set_page_config(layout="wide") st_set_css_and_footer() inputs = AppInputs() with st.sidebar: st_sidebar(inputs) st_main_page(inputs) if __name__ == "__main__": main()