From cda275f1cc60b06dd3b4bfb32d77a1955cd0927f Mon Sep 17 00:00:00 2001 From: ajaysi Date: Thu, 1 May 2025 20:41:41 +0530 Subject: [PATCH] AI Blog Writer enhancements & Streamlit UI updates --- .gitignore | 2 +- .../ai_agents_crew_writer.cpython-312.pyc | Bin 6574 -> 0 bytes .../ai_news_article_writer.cpython-312.pyc | Bin 4209 -> 0 bytes .../blog_from_google_serp.cpython-312.pyc | Bin 2523 -> 0 bytes .../combine_blog_and_keywords.cpython-312.pyc | Bin 1532 -> 0 bytes .../combine_research_and_blog.cpython-312.pyc | Bin 2629 -> 0 bytes .../keywords_to_blog.cpython-312.pyc | Bin 5410 -> 0 bytes .../long_form_ai_writer.cpython-312.pyc | Bin 12132 -> 0 bytes .../ai_blog_writer/ai_blog_generator.py | 34 +- .../ai_blog_writer/ai_blog_generator_utils.py | 527 ++++++- .../ai_blog_writer/blog_ai_research_utils.py | 420 +++++ .../blog_from_google_serp.py | 2 +- .../keywords_to_blog_streamlit.py | 872 ++++++++++ lib/ai_writers/keywords_to_blog_streamlit.py | 1397 ----------------- lib/ai_writers/long_form_ai_writer.py | 2 +- .../main_audio_to_blog.cpython-312.pyc | Bin 3816 -> 0 bytes ..._blogs_from_youtube_videos.cpython-312.pyc | Bin 4550 -> 0 bytes .../speech_to_blog/main_audio_to_blog.py | 17 +- .../get_blog_category.cpython-312.pyc | Bin 1611 -> 0 bytes .../get_blog_meta_desc.cpython-312.pyc | Bin 1575 -> 0 bytes .../get_blog_metadata.cpython-312.pyc | Bin 1470 -> 0 bytes .../get_blog_title.cpython-312.pyc | Bin 3000 -> 0 bytes .../__pycache__/get_tags.cpython-312.pyc | Bin 1464 -> 0 bytes lib/blog_metadata/get_blog_metadata.py | 2 +- .../blog_proof_reader.cpython-312.pyc | Bin 2690 -> 0 bytes .../__pycache__/humanize_blog.cpython-312.pyc | Bin 3020 -> 0 bytes .../save_blog_to_file.cpython-312.pyc | Bin 5412 -> 0 bytes .../stt_audio_blog.cpython-312.pyc | Bin 9953 -> 0 bytes .../ai_essay_writer.cpython-312.pyc | Bin 7280 -> 0 bytes .../ai_story_writer.cpython-312.pyc | Bin 7452 -> 0 bytes .../gemini_pro_text.cpython-312.pyc | Bin 2436 -> 0 bytes .../main_text_generation.cpython-312.pyc | Bin 4321 -> 0 bytes .../openai_text_gen.cpython-312.pyc | Bin 3908 -> 0 bytes .../gen_dali3_images.cpython-312.pyc | Bin 2530 -> 0 bytes .../gen_stabl_diff_img.cpython-312.pyc | Bin 1884 -> 0 bytes ...generate_image_from_prompt.cpython-312.pyc | Bin 3366 -> 0 bytes .../__pycache__/save_image.cpython-312.pyc | Bin 1902 -> 0 bytes 37 files changed, 1835 insertions(+), 1440 deletions(-) delete mode 100644 lib/ai_writers/__pycache__/ai_agents_crew_writer.cpython-312.pyc delete mode 100644 lib/ai_writers/__pycache__/ai_news_article_writer.cpython-312.pyc delete mode 100644 lib/ai_writers/__pycache__/blog_from_google_serp.cpython-312.pyc delete mode 100644 lib/ai_writers/__pycache__/combine_blog_and_keywords.cpython-312.pyc delete mode 100644 lib/ai_writers/__pycache__/combine_research_and_blog.cpython-312.pyc delete mode 100644 lib/ai_writers/__pycache__/keywords_to_blog.cpython-312.pyc delete mode 100644 lib/ai_writers/__pycache__/long_form_ai_writer.cpython-312.pyc create mode 100644 lib/ai_writers/ai_blog_writer/blog_ai_research_utils.py rename lib/ai_writers/{ => ai_blog_writer}/blog_from_google_serp.py (99%) create mode 100644 lib/ai_writers/ai_blog_writer/keywords_to_blog_streamlit.py delete mode 100644 lib/ai_writers/keywords_to_blog_streamlit.py delete mode 100644 lib/ai_writers/speech_to_blog/__pycache__/main_audio_to_blog.cpython-312.pyc delete mode 100644 lib/ai_writers/speech_to_blog/__pycache__/write_blogs_from_youtube_videos.cpython-312.pyc delete mode 100644 lib/blog_metadata/__pycache__/get_blog_category.cpython-312.pyc delete mode 100644 lib/blog_metadata/__pycache__/get_blog_meta_desc.cpython-312.pyc delete mode 100644 lib/blog_metadata/__pycache__/get_blog_metadata.cpython-312.pyc delete mode 100644 lib/blog_metadata/__pycache__/get_blog_title.cpython-312.pyc delete mode 100644 lib/blog_metadata/__pycache__/get_tags.cpython-312.pyc delete mode 100644 lib/blog_postprocessing/__pycache__/blog_proof_reader.cpython-312.pyc delete mode 100644 lib/blog_postprocessing/__pycache__/humanize_blog.cpython-312.pyc delete mode 100644 lib/blog_postprocessing/__pycache__/save_blog_to_file.cpython-312.pyc delete mode 100644 lib/gpt_providers/audio_to_text_generation/__pycache__/stt_audio_blog.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_generation/__pycache__/ai_essay_writer.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_generation/__pycache__/ai_story_writer.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_generation/__pycache__/gemini_pro_text.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_generation/__pycache__/main_text_generation.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_generation/__pycache__/openai_text_gen.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_to_image_generation/__pycache__/gen_dali3_images.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_to_image_generation/__pycache__/gen_stabl_diff_img.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_to_image_generation/__pycache__/main_generate_image_from_prompt.cpython-312.pyc delete mode 100644 lib/gpt_providers/text_to_image_generation/__pycache__/save_image.cpython-312.pyc diff --git a/.gitignore b/.gitignore index 8d440971..b714f846 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,4 @@ venv_* AI-Writer_cursor_workspace.code-workspace *.code-workspace .cursorignore -lib/ai_writers/__pycache__/ai_agents_crew_writer.cpython-312.pyc +lib/ai_writers/__pycache__/*.pyc diff --git a/lib/ai_writers/__pycache__/ai_agents_crew_writer.cpython-312.pyc b/lib/ai_writers/__pycache__/ai_agents_crew_writer.cpython-312.pyc deleted file mode 100644 index a285b32142c80aefe1f41601066dc6be085613a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6574 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 PsblKQ-eN|bkuv`eYUj2} diff --git a/lib/ai_writers/__pycache__/ai_news_article_writer.cpython-312.pyc b/lib/ai_writers/__pycache__/ai_news_article_writer.cpython-312.pyc deleted file mode 100644 index 8c8a1399bc5bdd24d7149aa906c89dbf3888a3ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4209 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 KO-aMTqW=KNgN021 diff --git a/lib/ai_writers/__pycache__/blog_from_google_serp.cpython-312.pyc b/lib/ai_writers/__pycache__/blog_from_google_serp.cpython-312.pyc deleted file mode 100644 index 4bf5e071c5eccc1ae1ccd3d10f34360413250c9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2523 zcmaJ@O=ufO6rPnU)y+)Uj-Nqgv<{gG@6UC_?o+xPRm_rB3j zgM&E)5C8q6`agXL{i+u6lZqz$-@xQ+WFk|eXfYhMMJ-q-785WgXwuOabqysD(U${_ zv666n7A+%*fy1Y2MYQFi|L&SeQ~xgYEE%3?$V{7kG~;9!voOy*K#RGyR>;0q$MXfv zPg7=Dgn!YQoup)y(5dxc*ql5UE;lAEPKZ0TUbiXPEUi=9RYTo#Ytm*e$c5({c-YT`QO4~K@zZt29YelOfITobK&exaWKC8q;?~q={3z&@)B8We3!u|B&|`pU zLxXqnNoq48wODld;CIocsXvR(HIzj%P6XOW#QU4cjpQ9Piex(0a~AROP5pr0OnlNC z&D4he7(MA~(U9zqPtA0!TSIq|*|({GrMLUSv8A=pTJkPxX@yMZ(iP@Z0XQrh1Y7nh zaWQihl9&@g3|?zs!!@zhl(-7{z%}Md;!0y%M<|D*r!266?@ZsoWUWcK48Dj2af{oM z@G(4#TQ;S*$=RxH65M7UY{&+7+Bn}L)trnDVMwi!DfC^e{H*mi5w15YmEner{s)7Dix!zg7f zTqWvldEm9eMU=V%pA?EXuze62EE>$C%AV6C;DuoswkrS=2wuqvT<6R=9LoXg=VP7V zRII8>aMchH19NfyCSgs$A_&0{D>#;5-ec}Rh(asXe);43C; zLHcAn0wc-{G?b|T>V#--uP623_ZR4CXt%z#ONB5V$6f>Kw^LBI|Ps2~8` zZOaBsia2U9#XXTIS%a9N768gwl>y1U8n{=?b;~xFTL8Ma9kbJ1NEao1rgQpCSLoUy z#sFbp3?-85iF7VsBQD_pxJs(xHU+ujg0Oia6^fe+9D)FX7jU_vZ^~u5J%sw_Sh%?q%d>r`Ku(sEN6Lig3=juM<#NNclsdtx8lL-8Ezu zLJ)pB#L3UZsrHkgK&VS4Amg{wS4v9@v-4MPEZkWVZ6S#>aek>hJH8O`@DjDFONLzu zSy?QpxGRb)Dj~;K1mVqMv+chdNK}IJ14sNg7&-z!u?nwyXip!0=j`4H`e0&bbYc%_ z7w|4F?wmRY(~q-zr;iQ|+?)CHEK2o1oV!1_bL6uZ`rKY3l{)h3*r})KUykLsGP}bk zw}$_E)qngClz{!r(DuNift{06&&6}HbK>fY%=D|wz}BtpzDIq3@1;QGtx!HZ9T-R7 zXUn6>A4ba~$sb2jFfH`^2`2m`gzf9gzz@M+2#IwEzGB diff --git a/lib/ai_writers/__pycache__/combine_blog_and_keywords.cpython-312.pyc b/lib/ai_writers/__pycache__/combine_blog_and_keywords.cpython-312.pyc deleted file mode 100644 index 9a2b4028c05cb8136a1a6e77111cacb271453515..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1532 zcmZWp&ube;6rR!k@Jf}fSZPS=Ce1)Yg=nfmLVHk+shc`ra2==%CPET_8>O71q!)|8bfg}eY>lb-E?4Q-pqUR-uvGB zc7B_k%^(=!&vzRo6`>d5CK=j9IQ$-jACQGC?4U}#;tGa+%1OCOMZqYAsIqM&7f;rM zi8!%{Vx4G1iQ?_-e^0`lrC91O+JO>lFtT)O#+h-AiUG254^`4VTui?V)$2tZ=nl7S zD!%BiE;_VBou%Dq87{sbZwHIEpwwI1Z7_!p%exNq!llvn>XLB}>|*_Z?7GzVO`8sj zslcdnhYQxHf!^T4HRa0`#Dj2I$8l?t?#P-=y?S_&$dmpLsT6-c-+9lDVvJhSiQ@ZLHiX zk*V)h(q)cAT7q|&MM;mh;XpQt+arb385N?t1&8^P@CLay;ODtDO_`j-k*)vF*wb>>9f{~!!p3(HhXQZ194 zaZrG1QyF%*#XX;rhTtw)Tl=g`){Cl`0SX1_m7O|;u5eF;gbg$*1Q$guP(#0gk@yUB z=!)BtfsuF%6fpHeW=707UcFJSZmzC>dTVo|>i2v}-Rsw?%d78gMx3ZRY^!RrTFel? zT92t%i?RpSO_MLRdO`mGn8Z8;%E9NGFzunSs^>3^v*?XW-*v_azjbkR@yf$8NbjV_ zIb$xncXOQAwEXk4mw(iMKU>&0M!EC*xxb&k_WEBa1?R@xgY5n6)AMUjHlD0JJ@@$^ z#>QxFen0)sSOfL3pNA;lW-p+J>1Cw+wD6Jgs|G?b6QsBwD1a||Y}%q7mrWPc4d!8Q1kY7;UGzm&WbR7CtQxhC_b&&T92YD$FlXLJZDAaMq!V`I_fR@krEJ%cJ>oqg>@k UQ?P!NL3kY>>Su6XEP#`L0l#;x^#A|> diff --git a/lib/ai_writers/__pycache__/combine_research_and_blog.cpython-312.pyc b/lib/ai_writers/__pycache__/combine_research_and_blog.cpython-312.pyc deleted file mode 100644 index 02c74633b857ddc1d52ab8cd9840361459506440..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2629 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 O!=I$69EE=Zy8jQE&Ut76 diff --git a/lib/ai_writers/__pycache__/long_form_ai_writer.cpython-312.pyc b/lib/ai_writers/__pycache__/long_form_ai_writer.cpython-312.pyc deleted file mode 100644 index 57f64dab313692cea0dfd87bf49bf026f12677c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12132 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 diff --git a/lib/ai_writers/ai_blog_writer/ai_blog_generator.py b/lib/ai_writers/ai_blog_writer/ai_blog_generator.py index f6a16bb1..287788a0 100644 --- a/lib/ai_writers/ai_blog_writer/ai_blog_generator.py +++ b/lib/ai_writers/ai_blog_writer/ai_blog_generator.py @@ -25,7 +25,6 @@ def display_input_section(): # First column: Keywords input with col1: st.markdown("### 📌 Content Source") - st.markdown("#### Enter Keywords, Title or URL") user_input = st.text_area( 'Power your content with keywords or a website URL', help='Provide keywords, a blog title, YouTube link, or web URL to generate targeted content.', @@ -36,7 +35,6 @@ def display_input_section(): # Second column: File uploader with col2: st.markdown("### 📁 File Upload") - st.markdown("#### Upload Reference Content") uploaded_file = st.file_uploader( "Add files to enhance your content", type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"], @@ -46,7 +44,6 @@ def display_input_section(): # Third column: Voice input with col3: st.markdown("### 🎤 Voice") - st.markdown("#### Record Ideas") audio_input = record_voice() if audio_input: st.success("Voice recorded!") @@ -54,13 +51,20 @@ def display_input_section(): return user_input, uploaded_file, audio_input -def display_content_type_selection(): - """Display the content type selection section and return the selected type.""" +def display_content_type_selection(inside_expander=False): + """Display the content type selection section and return the selected type. + + Args: + inside_expander (bool): If True, adjust heading levels for display inside an expander. + """ # Content options in a cleaner layout - st.markdown("### 🔧 Content Configuration") + if not inside_expander: + st.markdown("### 🔧 Content Configuration") + st.markdown("#### Select Content Type") + else: + st.markdown("#### Content Type") # Content type selection with better UI - st.markdown("#### Select Content Type") content_type = st.radio( "Choose the format and length of your blog content", ["Standard Blog Post", "Comprehensive Long-form", "AI Agent Team (Beta)"], @@ -556,8 +560,11 @@ def display_search_settings_tab(): def display_advanced_options(): """Display all advanced options tabs and return the selected configurations.""" - with st.expander("⚙️ Advanced Options", expanded=False): - tabs = st.tabs(["Content Characteristics", "Content & Analysis Options", "Blog Images Details", "LLM Options", "Search Settings"]) + + with st.expander("⚙️ Advanced Options for Personalization, Analysis, Images, LLM, and Search", expanded=False): + content_type, selected_content_type = display_content_type_selection(inside_expander=True) + + tabs = st.tabs(["Personalization", "Analysis Options", "Blog Images Details", "LLM Options", "Search Settings"]) with tabs[0]: # Content Characteristics blog_params = display_content_characteristics_tab() @@ -574,7 +581,7 @@ def display_advanced_options(): with tabs[4]: # Search Settings search_params = display_search_settings_tab() - return blog_params, content_analysis_params, image_params, llm_params, search_params + return content_type, selected_content_type, blog_params, content_analysis_params, image_params, llm_params, search_params def blog_from_keyword(): @@ -583,15 +590,12 @@ def blog_from_keyword(): # Get user inputs user_input, uploaded_file, audio_input = display_input_section() - # Get content type selection - content_type, selected_content_type = display_content_type_selection() - # Display advanced options and get configurations - blog_params, content_analysis_params, image_params, llm_params, search_params = display_advanced_options() + content_type, selected_content_type, blog_params, content_analysis_params, image_params, llm_params, search_params = display_advanced_options() # Generate button with icon and clearer purpose st.markdown("") # Add spacing - generate_pressed = st.button("✨ Generate Professional Blog Content", use_container_width=True) + generate_pressed = st.button("✨ Generate Blog Content", use_container_width=True) # Processing logic if generate_pressed: diff --git a/lib/ai_writers/ai_blog_writer/ai_blog_generator_utils.py b/lib/ai_writers/ai_blog_writer/ai_blog_generator_utils.py index d66ef376..cfe778e9 100644 --- a/lib/ai_writers/ai_blog_writer/ai_blog_generator_utils.py +++ b/lib/ai_writers/ai_blog_writer/ai_blog_generator_utils.py @@ -1,18 +1,24 @@ import re import os import json +import asyncio from loguru import logger import PyPDF2 import streamlit as st import tiktoken import openai +from datetime import datetime from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen -from lib.ai_writers.keywords_to_blog_streamlit import write_blog_from_keywords +# Remove the circular import +# from lib.ai_writers.ai_blog_writer.keywords_to_blog_streamlit import write_blog_from_keywords from lib.ai_writers.speech_to_blog.main_audio_to_blog import generate_audio_blog from lib.ai_writers.long_form_ai_writer import long_form_generator from lib.ai_writers.web_url_ai_writer import blog_from_url from lib.ai_writers.image_ai_writer import blog_from_image +from .blog_from_google_serp import write_blog_google_serp +from lib.blog_metadata.get_blog_metadata import blog_metadata +from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image # Constants CONFIG_PATH = os.path.join("lib", "workspace", "alwrity_config", "main_config.json") @@ -259,21 +265,247 @@ def process_keywords_input(user_input, search_params, blog_params, selected_cont st.error('Please provide at least two keywords for best results') return False + # Check for dialog states and handle them directly + if st.session_state.get("show_title_dialog", False): + st.warning("Please use the main function to handle title refinement dialog") + # Clear the dialog state to avoid getting stuck + st.session_state.show_title_dialog = False + return False + + if st.session_state.get("show_meta_dialog", False): + st.warning("Please use the main function to handle meta description refinement dialog") + # Clear the dialog state to avoid getting stuck + st.session_state.show_meta_dialog = False + return False + + if st.session_state.get("show_snippet_dialog", False): + st.warning("Please use the main function to handle structured data dialog") + # Clear the dialog state to avoid getting stuck + st.session_state.show_snippet_dialog = False + return False + try: if selected_content_type == "Normal-length content": st.subheader("Your Generated Blog Post") logger.info(f"Generating standard blog post with parameters: {blog_params}") - # Ensure all blog parameters are properly passed - # This is important as the UI may have settings that aren't in the default blog_params - short_blog = write_blog_from_keywords( - user_input, - search_params=search_params, - blog_params=blog_params - ) - st.markdown(short_blog) - return True - + # Use a direct approach to generate blog content to avoid nested expanders + # Instead of importing write_blog_from_keywords which contains many expanders + try: + # Show simplified progress UI + progress_container = st.container() + with progress_container: + progress_bar = st.progress(0) + status_text = st.empty() + + # Step 1: Initialize and show progress + status_text.info("Initializing blog generation...") + progress_bar.progress(0.1) + + # Initialize parameters + from .blog_ai_research_utils import initialize_parameters + search_params, blog_params = initialize_parameters(search_params, blog_params) + + # Step 2: Research phase + status_text.info("Researching your topic...") + progress_bar.progress(0.2) + + # Perform research using direct function calls + from .blog_ai_research_utils import do_google_serp_search, do_tavily_ai_search + + # Do Google search + status_text.info("Searching Google for relevant information...") + google_result = do_google_serp_search(user_input, max_results=search_params.get("max_results", 10)) + google_success = google_result and 'results' in google_result and google_result['results'] + progress_bar.progress(0.4) + + # Do Tavily search if needed + tavily_result = None + tavily_success = False + if not google_success: + status_text.info("Performing additional research with Tavily...") + tavily_result, _, _ = do_tavily_ai_search( + user_input, + max_results=search_params.get("max_results", 10), + search_depth=search_params.get("search_depth", "basic") + ) + tavily_success = tavily_result is not None + progress_bar.progress(0.5) + + # Step 3: Generate content + status_text.info("Generating blog content...") + progress_bar.progress(0.6) + + # Generate content based on search results + from .blog_from_google_serp import write_blog_google_serp + + if google_success: + blog_content = write_blog_google_serp(user_input, google_result['results'], blog_params=blog_params) + elif tavily_success: + blog_content = write_blog_google_serp(user_input, tavily_result, blog_params=blog_params) + else: + status_text.error("Failed to gather research data. Please try again.") + return False + + # Step 4: Generate metadata and image + status_text.info("Adding metadata and final touches...") + progress_bar.progress(0.8) + + # Import functions from keywords_to_blog_streamlit + from .keywords_to_blog_streamlit import generate_audio_version + + # Define a simple update_progress function for compatibility + def simple_update_progress(step, total, message): + status_text.info(message) + progress_bar.progress(step / total) + + # Generate metadata and image + # Import only essential functions needed for core processing + from .ai_blog_generator_utils import generate_blog_metadata, generate_blog_image + try: + # Create a proper status object + with st.status("Generating metadata and image...", expanded=True) as status: + # Generate metadata + blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata( + blog_content, user_input, status) + + # Generate featured image if metadata is available + generated_image_filepath = None + if blog_title and blog_meta_desc: + generated_image_filepath = generate_blog_image( + blog_title, blog_meta_desc, blog_content, status, blog_tags) + + # Save blog content to file + saved_blog_to_file = None + from ...blog_postprocessing.save_blog_to_file import save_blog_to_file + if blog_title and blog_meta_desc: + saved_blog_to_file = save_blog_to_file( + blog_content, blog_title, blog_meta_desc, blog_tags, + blog_categories, generated_image_filepath) + + # Create metadata dictionary with string conversions for table display + metadata = { + "blog_title": blog_title or "", + "blog_meta_desc": blog_meta_desc or "", + "blog_tags": ", ".join(blog_tags) if isinstance(blog_tags, list) else str(blog_tags or ""), + "blog_categories": ", ".join(blog_categories) if isinstance(blog_categories, list) else str(blog_categories or ""), + "blog_hashtags": blog_hashtags or "", + "blog_slug": blog_slug or "" + } + except Exception as e: + logger.error(f"Error generating metadata or image: {e}") + metadata = { + "blog_title": "Generated Blog", + "blog_meta_desc": "", + "blog_tags": "", + "blog_categories": "", + "blog_hashtags": "", + "blog_slug": "" + } + generated_image_filepath = None + saved_blog_to_file = None + + # Clear progress indicators + progress_bar.empty() + status_text.empty() + + # Final message + final_message = st.empty() + final_message.success("Blog generation complete!") + + # Display blog content first (without using expanders) + st.markdown("## Content") + st.markdown(blog_content) + + # Show file save information if available + if saved_blog_to_file: + st.success(f"✅ Blog saved to: {saved_blog_to_file}") + + # Add the audio generation button + st.markdown("---") + audio_col1, audio_col2 = st.columns([1, 3]) + with audio_col1: + generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True) + + with audio_col2: + if generate_audio_button: + generate_audio_version(blog_content) + + # Display metadata success message + if metadata["blog_title"]: + st.success(f"✅ Generated metadata for: {metadata['blog_title']}") + + # Display metadata table (without nesting expanders) + st.markdown("---") + st.subheader("🏷️ Blog SEO Metadata") + st.table({ + "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"], + "Value": [ + metadata["blog_title"], + metadata["blog_meta_desc"], + metadata["blog_tags"], + metadata["blog_categories"], + metadata["blog_hashtags"], + metadata["blog_slug"] + ] + }) + + # Display image if available + if generated_image_filepath: + st.subheader("🖼️ Featured Image") + st.image(generated_image_filepath, caption=metadata["blog_title"] or "Featured Image", use_column_width=True) + + # Add regenerate button + if st.button("🔄 Regenerate Image", key="regenerate_image_simplified"): + # Use the function directly to avoid any nested expanders + new_image_path = regenerate_blog_image( + metadata["blog_title"], + metadata["blog_meta_desc"], + blog_content, + metadata["blog_tags"] + ) + if new_image_path: + st.success("✅ Image regenerated successfully!") + st.image(new_image_path, caption=metadata["blog_title"], use_column_width=True) + else: + st.subheader("🖼️ Featured Image") + st.info("No image was generated. Try regenerating the blog.") + + # Add refinement buttons directly, without using helper functions + col1, col2 = st.columns(2) + with col1: + if st.button("🔄 Refine Blog Title", key="refine_title_simplified", use_container_width=True): + st.session_state.show_title_dialog = True + st.rerun() + with col2: + if st.button("🔄 Refine Meta Description", key="refine_meta_simplified", use_container_width=True): + st.session_state.show_meta_dialog = True + st.rerun() + + # Add structured data section directly, without using helper functions + st.markdown("---") + st.markdown("### Get Structured Data") + + structured_data_col1, structured_data_col2 = st.columns([3, 1]) + with structured_data_col1: + st.info("Rich snippets boost visibility and click-through rates in search results.") + with structured_data_col2: + if st.button("📊 Generate Rich Snippet", key="snippet_simplified", use_container_width=True): + st.session_state.show_snippet_dialog = True + st.rerun() + + # Clear the success message after a delay + import time + time.sleep(3) + final_message.empty() + + return True + + except Exception as inner_err: + logger.error(f"Error in simplified blog generation: {inner_err}") + st.error(f"Failed to generate blog content: {inner_err}") + return False + elif selected_content_type == "Long-form content": logger.info(f"Generating long-form content with parameters: {blog_params}") @@ -283,11 +515,20 @@ def process_keywords_input(user_input, search_params, blog_params, selected_cont search_params=search_params, blog_params=blog_params ) - st.success(f"Successfully generated long-form content for: {user_input}") + + # Show success message briefly then clear it + success_msg = st.empty() + success_msg.success(f"Successfully generated long-form content for: {user_input}") + # Clear the message after 3 seconds + import time + time.sleep(3) + success_msg.empty() + return True else: - st.info("AI Agent Team feature is coming soon! This will provide multi-perspective content with different AI experts collaborating on your blog.") + info_msg = st.empty() + info_msg.info("AI Agent Team feature is coming soon! This will provide multi-perspective content with different AI experts collaborating on your blog.") return False except Exception as err: @@ -298,7 +539,10 @@ def process_keywords_input(user_input, search_params, blog_params, selected_cont def process_pdf_input(uploaded_file): """Process a PDF file and generate content.""" - with st.expander("Processing PDF Document", expanded=True): + # Replace expander with a container to avoid nested expanders + pdf_container = st.container() + with pdf_container: + st.subheader("Processing PDF Document") pdf_reader = PyPDF2.PdfReader(uploaded_file) text = "" combined_result = "" @@ -361,22 +605,263 @@ def handle_content_generation(input_type, user_input, uploaded_file, search_para Returns: bool: True if content generation was successful, False otherwise """ - with st.spinner("Crafting your blog content... Please wait."): + # Create a status placeholder instead of a permanent message + status_message = st.empty() + status_message.info("Crafting your blog content... Please wait.") + + try: if input_type == "keywords": - return process_keywords_input(user_input, search_params, blog_params, selected_content_type) + result = process_keywords_input(user_input, search_params, blog_params, selected_content_type) + # Clear the status message when done + status_message.empty() + return result elif input_type == "youtube_url" or input_type == "audio_file": - return process_youtube_or_audio(user_input) + result = process_youtube_or_audio(user_input) + status_message.empty() + return result elif input_type == "web_url": - return process_web_url(user_input) + result = process_web_url(user_input) + status_message.empty() + return result elif input_type == "image_file": - return process_image_input(user_input, uploaded_file) + result = process_image_input(user_input, uploaded_file) + status_message.empty() + return result elif input_type == "PDF_file": - return process_pdf_input(uploaded_file) + result = process_pdf_input(uploaded_file) + status_message.empty() + return result else: + status_message.empty() st.error(f"Unsupported input type: {input_type}") - return False \ No newline at end of file + return False + except Exception as e: + status_message.empty() + st.error(f"An error occurred during content generation: {str(e)}") + return False + + +def generate_blog_content(search_keywords, google_search_result, tavily_search_result, + google_search_success, tavily_search_success, blog_params, status): + """ + Generate blog content using either Google or Tavily search results. + + Args: + search_keywords (str): Search keywords + google_search_result: Results from Google search + tavily_search_result: Results from Tavily search + google_search_success (bool): Whether Google search was successful + tavily_search_success (bool): Whether Tavily search was successful + blog_params (dict): Blog parameters + status: Streamlit status object + + Returns: + str: Generated blog content or None if generation failed + """ + # Check if both searches failed - if so, stop the process + if not google_search_success and not tavily_search_success: + st.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.") + st.warning("Please check your API keys in the environment settings and try again.") + return None + + # Try Google results first if available + if google_search_success and 'results' in google_search_result: + try: + status.update(label=f"✏️ Writing blog from Google Search results...") + # Pass blog parameters to the blog writing function + blog_style_info = f""" + Length: {blog_params.get('blog_length')} words + Tone: {blog_params.get('blog_tone')} + Target Audience: {blog_params.get('blog_demographic')} + Blog Type: {blog_params.get('blog_type')} + Language: {blog_params.get('blog_language')} + """ + status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...") + blog_markdown_str = write_blog_google_serp(search_keywords, google_search_result['results'], blog_params=blog_params) + status.update(label="✅ Generated content from Google search results", state="complete") + return blog_markdown_str + except Exception as err: + status.update(label=f"❌ Failed to generate content from Google results: {str(err)}", state="error") + st.error(f"Failed to generate content from Google results: {err}") + logger.error(f"Failed to process Google search results: {err}") + + # If Google failed or had no results, try Tavily + if tavily_search_success and tavily_search_result: + try: + status.update(label=f"✏️ Writing blog from Tavily search results...") + status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...") + blog_markdown_str = write_blog_google_serp(search_keywords, tavily_search_result, blog_params=blog_params) + status.update(label="✅ Generated content from Tavily search results", state="complete") + return blog_markdown_str + except Exception as err: + status.update(label=f"❌ Failed to generate content from Tavily results: {str(err)}", state="error") + st.error(f"Failed to generate content from Tavily results: {err}") + logger.error(f"Failed to process Tavily search results: {err}") + + # If we still don't have content, show error + st.error("⛔ Failed to generate any blog content from the research results.") + return None + + +def generate_blog_metadata(blog_markdown_str, search_keywords, status): + """ + Generate metadata for the blog content. + + Args: + blog_markdown_str (str): Blog content + search_keywords (str): Original search keywords + status: Streamlit status object + + Returns: + tuple: (blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug) + """ + status.update(label="🔍 Generating title, meta description, tags, categories, hashtags, and slug...") + try: + # Get all 6 metadata values from blog_metadata + blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = asyncio.run(blog_metadata(blog_markdown_str)) + status.update(label="✅ Generated blog metadata successfully") + return blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug + except Exception as err: + st.error(f"Failed to get blog metadata: {err}") + logger.error(f"Failed to get blog metadata: {err}") + status.update(label="❌ Failed to get blog metadata", state="error") + return None, None, None, None, None, None + + +def generate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags=None): + """ + Generate a featured image for the blog. + + Args: + blog_title (str): Blog title + blog_meta_desc (str): Blog meta description + blog_markdown_str (str): Blog content + status: Streamlit status object + blog_tags (list, optional): Blog tags to use for image prompt enhancement + + Returns: + str: Path to the generated image or None if generation failed + """ + try: + status.update(label="🖼️ Generating featured image for blog...") + + # Create a better prompt for image generation + if blog_title and blog_meta_desc: + # If we have both title and description, use them + text_to_image = f"{blog_title}: {blog_meta_desc}" + elif blog_title: + # If we only have title, use it + text_to_image = blog_title + elif blog_meta_desc: + # If we only have description, use it + text_to_image = blog_meta_desc + else: + # Fallback to first 200 chars of content + text_to_image = blog_markdown_str[:200] + + # Ensure the prompt is of reasonable length + if len(text_to_image) > 300: + text_to_image = text_to_image[:300] + + # Log the prompt being used + logger.info(f"Generating image with prompt: {text_to_image}") + status.update(label=f"🖼️ Creating image with prompt: \"{text_to_image[:50]}...\"") + + # Extract blog tags if available + blog_tags_list = blog_tags if isinstance(blog_tags, list) else [] + + # Attempt image generation with all available parameters + generated_image_filepath = generate_image( + user_prompt=text_to_image, + title=blog_title, + description=blog_meta_desc, + tags=blog_tags_list, + content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads + ) + + # If first attempt failed, try with a simplified prompt + if not generated_image_filepath: + logger.warning("First image generation attempt failed, trying with simplified prompt") + status.update(label="⚠️ First image attempt failed, trying again with simplified prompt...") + + # Create a simpler prompt + simplified_prompt = " ".join(text_to_image.split()[:10]) + generated_image_filepath = generate_image( + user_prompt=simplified_prompt, + title=blog_title, + description=blog_meta_desc, + tags=blog_tags_list, + content=blog_markdown_str[:1000] # Use even shorter content for the retry + ) + + if generated_image_filepath: + status.update(label="✅ Successfully generated featured image") + return generated_image_filepath + else: + status.update(label="❌ Image generation failed - no image created", state="error") + return None + + except Exception as err: + st.warning(f"Failed in Image generation: {err}") + logger.error(f"Failed in Image generation: {err}") + status.update(label="❌ Image generation failed - no image created", state="error") + return None + + +def regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags=None): + """ + Regenerate a blog image on demand. + + Args: + blog_title (str): Blog title + blog_meta_desc (str): Blog meta description + blog_markdown_str (str): Blog content + blog_tags (list, optional): Blog tags to use for image prompt enhancement + + Returns: + str: Path to the generated image or None if generation failed + """ + with st.status("Regenerating image...", expanded=True) as status: + try: + # Use keywords from title or description + if blog_title: + keywords = " ".join(blog_title.split()[:6]) + prompt = f"Blog illustration for: {keywords}" + elif blog_meta_desc: + keywords = " ".join(blog_meta_desc.split()[:6]) + prompt = f"Blog illustration for: {keywords}" + else: + keywords = blog_markdown_str.split()[:50] + prompt = f"Blog illustration based on: {' '.join(keywords[:6])}" + + status.update(label=f"🖼️ Generating new image with prompt: \"{prompt}\"") + + # Extract any tags if available - will be passed as empty list otherwise + blog_tags_list = blog_tags if isinstance(blog_tags, list) else [] + + # Generate the image with all parameters + generated_image_filepath = generate_image( + user_prompt=prompt, + title=blog_title, + description=blog_meta_desc, + tags=blog_tags_list, + content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads + ) + + if generated_image_filepath: + status.update(label="✅ Successfully generated new image", state="complete") + return generated_image_filepath + else: + status.update(label="❌ Image regeneration failed", state="error") + return None + + except Exception as err: + st.error(f"Failed to regenerate image: {err}") + logger.error(f"Image regeneration error: {err}") + status.update(label="❌ Image regeneration failed", state="error") + return None \ No newline at end of file diff --git a/lib/ai_writers/ai_blog_writer/blog_ai_research_utils.py b/lib/ai_writers/ai_blog_writer/blog_ai_research_utils.py new file mode 100644 index 00000000..1582d14f --- /dev/null +++ b/lib/ai_writers/ai_blog_writer/blog_ai_research_utils.py @@ -0,0 +1,420 @@ +import sys +import os +import streamlit as st +from loguru import logger +from dotenv import load_dotenv +from pathlib import Path +import time + +# Load environment variables +load_dotenv(Path('../../../.env')) + +# Import necessary modules +from ...ai_web_researcher.gpt_online_researcher import ( + do_google_serp_search as gpt_do_google_serp_search, + do_tavily_ai_search as gpt_do_tavily_ai_search +) +from ...ai_web_researcher.tavily_ai_search import do_tavily_ai_search as tavily_direct_search + + +def initialize_parameters(search_params=None, blog_params=None): + """ + Initialize and validate search and blog parameters with defaults. + + Args: + search_params (dict, optional): Search parameters + blog_params (dict, optional): Blog parameters + + Returns: + tuple: (search_params, blog_params) with defaults applied + """ + # Initialize search params if not provided + if search_params is None: + search_params = {} + + # Initialize blog params if not provided + if blog_params is None: + blog_params = {} + + # Provide default values only for missing keys + # This ensures we don't override values that were intentionally set to 0 or other falsy values + if "max_results" not in search_params: + search_params["max_results"] = 10 + if "search_depth" not in search_params: + search_params["search_depth"] = "basic" + if "time_range" not in search_params: + search_params["time_range"] = "year" + if "include_domains" not in search_params: + search_params["include_domains"] = [] + + # Provide default values only for missing blog parameter keys + if "blog_length" not in blog_params: + blog_params["blog_length"] = 2000 + if "blog_tone" not in blog_params: + blog_params["blog_tone"] = "Professional" + if "blog_demographic" not in blog_params: + blog_params["blog_demographic"] = "Professional" + if "blog_type" not in blog_params: + blog_params["blog_type"] = "Informational" + if "blog_language" not in blog_params: + blog_params["blog_language"] = "English" + if "blog_output_format" not in blog_params: + blog_params["blog_output_format"] = "markdown" + + # Log the parameters for debugging + logger.info(f"Using search parameters: {search_params}") + logger.info(f"Using blog parameters: {blog_params}") + + return search_params, blog_params + + +def perform_google_search(search_keywords, search_params, status, status_container, progress_bar): + """ + Perform Google SERP search for the given keywords. + + Args: + search_keywords (str): Keywords to search for + search_params (dict): Search parameters + status: Streamlit status object + status_container: Streamlit container for status messages + progress_bar: Streamlit progress bar + + Returns: + tuple: (google_search_result, g_titles, success_flag) + """ + def update_progress(message, progress=None, level="info"): + """Helper function to update progress in Streamlit UI""" + if progress is not None: + progress_bar.progress(progress) + + if level == "error": + status_container.error(f"🚫 {message}") + elif level == "warning": + status_container.warning(f"⚠️ {message}") + elif level == "success": + status_container.success(f"✅ {message}") + else: + status_container.info(f"🔄 {message}") + logger.debug(f"Progress update [{level}]: {message}") + + try: + # Update the function call to include the required parameters and search_params + status.update(label=f"Starting Google SERP search for: {search_keywords}") + + # Add search params to the Google SERP search + google_search_params = { + "max_results": search_params.get("max_results", 10) + } + + # Include domains if provided + if search_params.get("include_domains"): + google_search_params["include_domains"] = search_params.get("include_domains") + + google_search_result = do_google_serp_search( + search_keywords, + status_container=status_container, + update_progress=update_progress, + **google_search_params + ) + + if google_search_result and google_search_result.get('titles') and len(google_search_result.get('titles', [])) > 0: + status.update(label=f"✅ Finished with Google web for Search: {search_keywords}") + g_titles = google_search_result.get('titles', []) + return google_search_result, g_titles, True + else: + # Check if there's an error message in the result + if google_search_result and 'summary' in google_search_result and 'Error' in google_search_result['summary']: + error_msg = google_search_result['summary'] + status.update(label=f"❌ Google search failed: {error_msg}", state="error") + st.error(f"Google SERP search failed: {error_msg}") + else: + status.update(label="❌ Failed to get Google SERP results. No valid data returned.", state="error") + st.error("Google SERP search failed to return valid results.") + return google_search_result, [], False + except Exception as err: + status.update(label=f"❌ Google search error: {str(err)}", state="error") + st.error(f"Google web research failed: {err}") + logger.error(f"Failed in Google web research: {err}") + return None, [], False + + +def perform_tavily_search(search_keywords, search_params, status): + """ + Perform Tavily AI search for the given keywords. + + Args: + search_keywords (str): Keywords to search for + search_params (dict): Search parameters + status: Streamlit status object + + Returns: + tuple: (tavily_search_result, success_flag) + """ + try: + status.update(label=f"🔍 Starting Tavily AI research: {search_keywords}") + + # Pass the search parameters to Tavily + tavily_result_tuple = do_tavily_ai_search( + search_keywords, + max_results=search_params.get("max_results", 10), + search_depth=search_params.get("search_depth", "basic"), + include_domains=search_params.get("include_domains", []), + time_range=search_params.get("time_range", "year") + ) + + if tavily_result_tuple and len(tavily_result_tuple) == 3: + tavily_search_result, t_titles, t_answer = tavily_result_tuple + # If we have either titles or an answer, consider it a success + if (t_titles and len(t_titles) > 0) or (t_answer and len(t_answer) > 10): + status.update(label=f"✅ Finished Tavily AI Search on: {search_keywords}", state="complete") + return tavily_search_result, True + else: + status.update(label="❌ Tavily search returned empty results", state="error") + st.warning("Tavily search didn't find relevant information.") + return tavily_search_result, False + else: + status.update(label="❌ Tavily search returned incomplete results", state="error") + st.error("Tavily search failed to return valid results.") + return None, False + + except Exception as err: + status.update(label=f"❌ Tavily search error: {str(err)}", state="error") + st.error(f"Failed in Tavily web research: {err}") + logger.error(f"Failed in Tavily web research: {err}") + return None, False + + +def do_google_serp_search(search_keywords, status_container=None, update_progress=None, **kwargs): + """ + Wrapper function to handle the parameter mismatch with the original function. + """ + try: + if status_container is None: + status_container = st.empty() + + if update_progress is None: + def update_progress(message, progress=None, level="info"): + if level == "error": + status_container.error(message) + elif level == "warning": + status_container.warning(message) + else: + status_container.info(message) + + # Create a fixed update_progress function that handles any progress type + def safe_update_progress(message, progress=None, level="info"): + try: + # Handle progress value of different types + if progress is not None: + if isinstance(progress, str): + # Try to convert string to float if it represents a number + try: + progress = float(progress) + except ValueError: + # If conversion fails, just log the message without updating progress + progress = None + + # Call the original update_progress with sanitized values + update_progress(message, progress, level) + except Exception as err: + # If there's an error in the progress function, just log to console + logger.error(f"Error in progress update: {err}") + # Try one more time with just the message + try: + update_progress(message, None, level) + except: + pass + + # Set default search parameters - fix the parameter to use 'max_results' not 'num_results' + search_params = { + "max_results": kwargs.get("max_results", 10), + "include_domains": kwargs.get("include_domains", []), + "search_depth": kwargs.get("search_depth", "basic") + } + + # Update status to indicate we're checking API keys + status_container.info("🔑 Checking required API keys...") + + # Call the original function with the required parameters + result = gpt_do_google_serp_search(search_keywords, status_container, safe_update_progress, **search_params) + return result + + except Exception as e: + error_msg = str(e) + logger.error(f"Error in do_google_serp_search wrapper: {error_msg}") + + # Check for common error patterns and display user-friendly messages + if "SERPER_API_KEY is missing" in error_msg: + status_container.error("🔑 Google search API key (SERPER_API_KEY) is missing. Please check your environment settings.") + st.error("Google SERP search failed: API key is missing. Using alternative methods.") + elif "Progress Value has invalid type" in error_msg: + # This is an internal error, log it but show a more user-friendly message + status_container.warning("⚠️ Internal progress tracking error. Continuing with search.") + else: + # For unknown errors, show the full error message + status_container.error(f"🚫 Google search error: {error_msg}") + st.error(f"Google SERP search failed: {error_msg}") + + # Return a minimal result structure to prevent downstream errors + return { + 'results': {}, + 'titles': [], + 'summary': f"Error occurred during search: {error_msg}", + 'stats': { + 'organic_count': 0, + 'questions_count': 0, + 'related_count': 0 + } + } + + +def do_tavily_ai_search(keywords, max_results=10, search_depth="basic", include_domains=None, time_range="year"): + """ + Wrapper function for Tavily search to handle parameter differences. + + Args: + keywords (str): Keywords to search for + max_results (int): Maximum number of search results to return + search_depth (str): "basic" or "advanced" search depth + include_domains (list): List of domains to prioritize in search + time_range (str): Time range for results ("day", "week", "month", "year", "all") + """ + status_container = st.empty() + + if include_domains is None: + include_domains = [] + + try: + # Show status message + status_container.info(f"🔍 Preparing Tavily AI search with {search_depth} depth...") + + # FIXED: Ensure all parameters have correct types to prevent comparison errors + tavily_params = { + 'max_results': int(max_results), # Explicitly convert to int + 'search_depth': str(search_depth), # Ensure this is a string + 'include_domains': include_domains, + 'time_range': str(time_range) + } + + # Log the parameters for debugging + logger.info(f"Tavily search parameters: {tavily_params}") + + # Check for API key before making the request + tavily_api_key = os.environ.get("TAVILY_API_KEY") + if not tavily_api_key: + status_container.error("🔑 Tavily API key (TAVILY_API_KEY) is missing. Please check your environment settings.") + st.error("Tavily search failed: API key is missing. Using alternative methods.") + return None, [], "API key missing" + + status_container.info(f"🔍 Searching with Tavily AI using {search_depth} depth for: {keywords}") + + # Direct implementation without calling gpt_do_tavily_ai_search to avoid type issues + try: + # Call the function directly with correct parameter types + tavily_raw_results = tavily_direct_search( + keywords, + max_results=tavily_params['max_results'], + search_depth=tavily_params['search_depth'], + include_domains=tavily_params['include_domains'], + time_range=tavily_params['time_range'] + ) + + # Extract the needed information + if isinstance(tavily_raw_results, tuple) and len(tavily_raw_results) == 3: + # If already in the right format, use it directly + return tavily_raw_results + + # Process the results to extract titles and answer + t_results = tavily_raw_results + t_titles = [] + t_answer = "" + + # Extract titles from results if available + if isinstance(t_results, dict): + if 'results' in t_results and isinstance(t_results['results'], list): + t_titles = [r.get('title', '') for r in t_results['results']] + status_container.success(f"✅ Found {len(t_titles)} relevant articles") + if 'answer' in t_results: + t_answer = t_results['answer'] + status_container.success("✅ Generated a summary answer") + + return t_results, t_titles, t_answer + + except ImportError: + # Fall back to the original function if direct import fails + status_container.warning("⚠️ Using fallback Tavily search method...") + logger.warning("Using fallback Tavily search method") + + # FIXED: Alternative approach - wrap the call in try/except to handle type errors + try: + tavily_result = gpt_do_tavily_ai_search(keywords, **tavily_params) + + # Format the result to match what the blog writer expects + if isinstance(tavily_result, tuple) and len(tavily_result) == 3: + status_container.success("✅ Tavily search completed successfully") + return tavily_result + + # If not a tuple with expected values, try to extract what we need + t_results = tavily_result + + # Extract titles and answer if available + t_titles = [] + t_answer = "" + + if isinstance(t_results, dict): + if 'results' in t_results and isinstance(t_results['results'], list): + t_titles = [r.get('title', '') for r in t_results['results']] + status_container.success(f"✅ Found {len(t_titles)} relevant articles") + if 'answer' in t_results: + t_answer = t_results['answer'] + status_container.success("✅ Generated a summary answer") + + return t_results, t_titles, t_answer + + except TypeError as type_err: + # Handle the specific type error more gracefully + error_msg = str(type_err) + logger.error(f"Type error in Tavily search: {error_msg}") + + if "'>' not supported" in error_msg: + status_container.error("🚫 Tavily search parameter type error. Trying alternative approach...") + + # Try a simpler approach with minimal parameters + try: + # Call with only the keyword and fixed max_results + tavily_result = gpt_do_tavily_ai_search(keywords, max_results=10) + + # Minimal processing to extract titles and answer + t_results = tavily_result + t_titles = [] + t_answer = "" + + if isinstance(t_results, dict): + if 'results' in t_results and isinstance(t_results['results'], list): + t_titles = [r.get('title', '') for r in t_results['results']] + if 'answer' in t_results: + t_answer = t_results['answer'] + + return t_results, t_titles, t_answer + except Exception as inner_err: + logger.error(f"Alternative Tavily approach also failed: {inner_err}") + raise + else: + # Re-raise other type errors + raise + + except Exception as e: + error_msg = str(e) + logger.error(f"Error in do_tavily_ai_search wrapper: {error_msg}") + + # Display user-friendly error message + status_container.error(f"🚫 Tavily search error: {error_msg}") + st.error(f"Tavily AI search failed: {error_msg}") + + # Return empty results to prevent downstream errors + return None, [], f"Error: {error_msg}" + + finally: + # Clear the status container after a delay + time.sleep(2) + status_container.empty() \ No newline at end of file diff --git a/lib/ai_writers/blog_from_google_serp.py b/lib/ai_writers/ai_blog_writer/blog_from_google_serp.py similarity index 99% rename from lib/ai_writers/blog_from_google_serp.py rename to lib/ai_writers/ai_blog_writer/blog_from_google_serp.py index 1ce1ae41..adb6e1f3 100644 --- a/lib/ai_writers/blog_from_google_serp.py +++ b/lib/ai_writers/ai_blog_writer/blog_from_google_serp.py @@ -10,7 +10,7 @@ logger.add(sys.stdout, format="{level}|{file}:{line}:{function}| {message}" ) -from ..gpt_providers.text_generation.main_text_generation import llm_text_gen +from ...gpt_providers.text_generation.main_text_generation import llm_text_gen def write_blog_google_serp(keywords, search_results, blog_params=None): diff --git a/lib/ai_writers/ai_blog_writer/keywords_to_blog_streamlit.py b/lib/ai_writers/ai_blog_writer/keywords_to_blog_streamlit.py new file mode 100644 index 00000000..df44084c --- /dev/null +++ b/lib/ai_writers/ai_blog_writer/keywords_to_blog_streamlit.py @@ -0,0 +1,872 @@ +import sys +import os +import asyncio +from textwrap import dedent +from pathlib import Path +from datetime import datetime +import streamlit as st +from gtts import gTTS +import base64 +from dotenv import load_dotenv +import time + +# Load environment variables +load_dotenv(Path('../../.env')) +# Logger setup +from loguru import logger +logger.remove() +logger.add(sys.stdout, + colorize=True, + format="{level}|{file}:{line}:{function}| {message}") + +# Import other necessary modules +from ...ai_web_researcher.gpt_online_researcher import ( + do_metaphor_ai_research, do_google_pytrends_analysis) +from .blog_from_google_serp import write_blog_google_serp, blog_with_research +from ...blog_metadata.get_blog_metadata import blog_metadata +from ...blog_postprocessing.save_blog_to_file import save_blog_to_file +from ...gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image +from ...ai_seo_tools.content_title_generator import generate_blog_titles +from ...ai_seo_tools.meta_desc_generator import generate_blog_metadesc +from ...ai_seo_tools.seo_structured_data import ai_structured_data + +# Import search functions from the research utils module +from .blog_ai_research_utils import ( + initialize_parameters, + perform_google_search, + perform_tavily_search, + do_google_serp_search, + do_tavily_ai_search +) + +# REMOVED CIRCULAR IMPORTS +# Import content and image generation functions from the generator utils module +# from .ai_blog_generator_utils import ( +# generate_blog_content, +# generate_blog_metadata, +# generate_blog_image, +# regenerate_blog_image +# ) + +def save_blog_content(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath, status, blog_hashtags=None, blog_slug=None): + """ + Save the blog content to a file. + + Args: + blog_markdown_str (str): Blog content + blog_title (str): Blog title + blog_meta_desc (str): Blog meta description + blog_tags (list): Blog tags + blog_categories (list): Blog categories + generated_image_filepath (str): Path to the generated image + status: Streamlit status object + blog_hashtags (str, optional): Social media hashtags + blog_slug (str, optional): SEO-friendly URL slug + + Returns: + str: Path to the saved file or None if saving failed + """ + try: + status.update(label="💾 Saving blog content to file...") + saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc, + blog_tags, blog_categories, generated_image_filepath) + status.update(label=f"✅ Saved the content to: {saved_blog_to_file}") + return saved_blog_to_file + except Exception as err: + st.error(f"Failed to save blog to file: {err}") + logger.error(f"Failed to save blog to file: {err}") + status.update(label="❌ Failed to save blog to file", state="error") + return None + + +def generate_audio_version(blog_markdown_str, status=None): + """ + Generate an audio version of the blog content. + + Args: + blog_markdown_str (str): Blog content + status: Streamlit status object (optional) + + Returns: + bool: True if audio generation was successful, False otherwise + """ + try: + if status: + status.update(label="🔊 Generating audio version of the blog...") + else: + st.info("🔊 Generating audio version...") + + # Only generate audio for reasonable-sized blogs (to avoid errors with very large text) + if blog_markdown_str and len(blog_markdown_str) < 50000: # Max ~50KB of text + tts = gTTS(text=blog_markdown_str[:40000], lang='en', slow=False) # Use first 40K chars to be safe + tts.save("delete_me.mp3") + st.audio("delete_me.mp3") + st.download_button( + label="📥 Download Audio File", + data=open("delete_me.mp3", "rb").read(), + file_name="blog_audio.mp3", + mime="audio/mp3" + ) + if status: + status.update(label="✅ Audio version generated successfully", state="complete") + else: + st.success("✅ Audio version generated successfully") + return True + else: + st.warning("Blog content too large for audio generation") + if status: + status.update(label="⚠️ Blog content too large for audio generation", state="complete") + return False + except Exception as err: + st.warning(f"Failed to generate audio version: {err}") + logger.error(f"Failed to generate audio version: {err}") + if status: + status.update(label="❌ Failed to generate audio version", state="error") + return False + + +# Helper functions for write_blog_from_keywords +def setup_progress_tracking(): + """Set up progress tracking elements for blog generation.""" + # Create a placeholder for the final blog content + final_content_placeholder = st.empty() + + # Create progress tracking + progress_placeholder = st.empty() + with progress_placeholder.container(): + progress_bar = st.progress(0) + status_text = st.empty() + + def update_progress(step, total_steps, message): + """Update the progress bar and status message""" + progress_value = min(step / total_steps, 1.0) + progress_bar.progress(progress_value) + status_text.info(f"Step {step}/{total_steps}: {message}") + + # When process is complete, clear the progress info + if step == total_steps: + import time + time.sleep(3) # Show the complete message for 3 seconds + progress_bar.empty() + status_text.empty() + + return final_content_placeholder, progress_placeholder, progress_bar, status_text, update_progress + + +def perform_research_phase(search_keywords, search_params, update_progress): + """ + Perform the research phase of blog generation. + + Args: + search_keywords (str): Keywords to research + search_params (dict): Search parameters + update_progress (function): Function to update progress + + Returns: + tuple: Google search results, Tavily search results, success flags, and blog titles + """ + update_progress(1, 5, f"Starting web research on '{search_keywords}'") + logger.info(f"Researching and Writing Blog on keywords: {search_keywords}") + + # Create a section header for the research phase + st.subheader("🔍 Web Research Progress") + + # Use a collapsible expander for research details + with st.expander("Research Details", expanded=True): + example_blog_titles = [] + + # Create a status element for research updates + with st.status("Web research in progress...", expanded=True) as status: + status.update(label=f"📊 Performing web research on: {search_keywords}") + + # Create status container and progress tracking for Google SERP + status_container = st.empty() + research_progress = st.progress(0) + + # Google Search + status.update(label="🔍 Performing Google search...") + google_search_result, g_titles, google_search_success = perform_google_search( + search_keywords, search_params, status, status_container, research_progress + ) + if g_titles: + example_blog_titles.append(g_titles) + status.update(label=f"✅ Google search complete - found {len(g_titles)} relevant resources") + else: + status.update(label="⚠️ Google search yielded limited results") + + # Tavily Search + status.update(label="🔍 Performing Tavily AI search...") + tavily_search_result, tavily_search_success = perform_tavily_search( + search_keywords, search_params, status + ) + + if tavily_search_success: + status.update(label="✅ Tavily AI search complete", state="complete") + elif google_search_success: + status.update(label="⚠️ Tavily search had issues, but Google search was successful") + else: + status.update(label="❌ Both search methods encountered issues", state="error") + + # Clear the progress indicators + status_container.empty() + research_progress.empty() + + return google_search_result, tavily_search_result, google_search_success, tavily_search_success, example_blog_titles + + +def generate_content_phase(search_keywords, google_search_result, tavily_search_result, + google_search_success, tavily_search_success, blog_params, update_progress): + """ + Generate blog content from research results. + + Args: + search_keywords (str): Keywords to research + google_search_result: Results from Google search + tavily_search_result: Results from Tavily search + google_search_success (bool): Whether Google search was successful + tavily_search_success (bool): Whether Tavily search was successful + blog_params (dict): Blog parameters + update_progress (function): Function to update progress + + Returns: + str: Generated blog content or None if generation failed + """ + # Import content generation function here to avoid circular import + from .ai_blog_generator_utils import generate_blog_content + + update_progress(2, 5, "Generating blog content from research") + + # Create a section header for the content generation phase + st.subheader("✍️ Content Generation Progress") + + # Use a collapsible expander for content generation details + with st.expander("Content Generation Details", expanded=True): + # Create a status element for content generation updates + with st.status("Content generation in progress...", expanded=True) as status: + if google_search_success: + source = "Google search results" + else: + source = "Tavily AI research" + + status.update(label=f"📝 Creating {blog_params.get('blog_tone')} {blog_params.get('blog_type')} content for {blog_params.get('blog_demographic')} audience...") + + blog_markdown_str = generate_blog_content( + search_keywords, google_search_result, tavily_search_result, + google_search_success, tavily_search_success, blog_params, status + ) + + if blog_markdown_str: + status.update(label=f"✅ Successfully generated ~{len(blog_markdown_str.split())} words of content using {source}", state="complete") + else: + status.update(label="❌ Content generation failed", state="error") + + return blog_markdown_str + + +def generate_metadata_and_image(blog_markdown_str, search_keywords, blog_tags, update_progress): + """ + Generate metadata and featured image for the blog. + + Args: + blog_markdown_str (str): Blog content + search_keywords (str): Keywords used for research + blog_tags (list): Blog tags + update_progress (function): Function to update progress + + Returns: + tuple: Blog metadata and image filepath + """ + # Import metadata and image generation functions here to avoid circular import + from .ai_blog_generator_utils import generate_blog_metadata, generate_blog_image + + update_progress(3, 5, "Generating SEO metadata and enhancements") + + # Create a section header for the enhancement phase + st.subheader("🔍 SEO & Enhancement Progress") + + # Use a collapsible expander for enhancement details + with st.expander("Enhancement Details", expanded=True): + blog_title = None + blog_meta_desc = None + blog_categories = None + blog_hashtags = None + blog_slug = None + generated_image_filepath = None + saved_blog_to_file = None + + # Create a status element for enhancement updates + with st.status("Enhancing content...", expanded=True) as status: + # Generate metadata + status.update(label="🏷️ Generating SEO metadata (title, description, tags)...") + blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata( + blog_markdown_str, search_keywords, status + ) + + # Check if there are updated values in session state + if 'blog_title' in st.session_state: + blog_title = st.session_state.blog_title + status.update(label=f"✅ Using refined title: \"{blog_title}\"") + + if 'blog_meta_desc' in st.session_state: + blog_meta_desc = st.session_state.blog_meta_desc + status.update(label=f"✅ Using refined meta description") + + if blog_title and blog_meta_desc: + status.update(label=f"✅ Generated metadata: \"{blog_title}\"") + + # Generate featured image + status.update(label="🖼️ Creating featured image...") + generated_image_filepath = generate_blog_image( + blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags + ) + + # Save blog content to file + status.update(label="💾 Saving blog content...") + saved_blog_to_file = save_blog_content( + blog_markdown_str, blog_title, blog_meta_desc, blog_tags, + blog_categories, generated_image_filepath, status, blog_hashtags, blog_slug + ) + + status.update(label="✅ Content enhancement complete", state="complete") + else: + status.update(label="⚠️ Metadata generation had issues, using simplified format", state="warning") + + # Add buttons for metadata refinement + create_metadata_refinement_ui() + + # Add rich snippet section + create_structured_data_ui() + + metadata = { + "blog_title": blog_title, + "blog_meta_desc": blog_meta_desc, + "blog_tags": blog_tags, + "blog_categories": blog_categories, + "blog_hashtags": blog_hashtags, + "blog_slug": blog_slug + } + + return metadata, generated_image_filepath, saved_blog_to_file + + +def create_metadata_refinement_ui(): + """Create UI elements for refining blog metadata (title and meta description).""" + col1, col2 = st.columns(2) + with col1: + if st.button("🔄 Refine Blog Title", key="refine_title_main", use_container_width=True): + st.session_state.show_title_dialog = True + st.rerun() + with col2: + if st.button("🔄 Refine Meta Description", key="refine_meta_main", use_container_width=True): + st.session_state.show_meta_dialog = True + st.rerun() + + +def create_structured_data_ui(): + """Create UI elements for generating structured data.""" + st.markdown("---") + structured_data_col1, structured_data_col2 = st.columns([3, 1]) + + with structured_data_col1: + # Educational popover explaining why rich snippets are important + with st.expander("ℹ️ Why Rich Snippets Are Important for SEO"): + st.markdown(""" + ### Rich Snippets: Boosting Your SEO and Click-Through Rates + + **What are Rich Snippets?** + + Rich snippets are enhanced search results that display additional information directly in search engine results pages (SERPs). They're created using structured data markup (JSON-LD) that helps search engines understand your content better. + + **Why are they important?** + + 1. **Increased Visibility**: Rich snippets stand out in search results with stars, images, and additional information + + 2. **Higher Click-Through Rates (CTR)**: Studies show rich snippets can increase CTR by 30-150% + + 3. **Improved SEO**: They help search engines understand your content better, potentially improving rankings + + 4. **Enhanced User Experience**: Users can see key information before clicking, leading to more qualified traffic + + 5. **Mobile-Friendly**: Rich snippets are especially effective on mobile searches + + **Common types of rich snippets include:** + - Articles/Blogs (with author, date, image) + - Products (with ratings, price, availability) + - Recipes (with cooking time, ratings, calories) + - Events (with date, location, ticket info) + - Local Business (with address, hours, ratings) + + Adding structured data to your content is a powerful SEO technique that requires minimal effort but provides significant benefits. + """) + + with structured_data_col2: + # Button to generate rich snippet + if st.button("📊 Generate Rich Snippet", key="snippet_main", use_container_width=True): + st.session_state.show_snippet_dialog = True + st.rerun() + + +def display_featured_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags, generated_image_filepath): + """ + Display the featured image with regeneration options. + + Args: + blog_title (str): Blog title + blog_meta_desc (str): Blog meta description + blog_markdown_str (str): Blog content + blog_tags (list): Blog tags + generated_image_filepath (str): Path to the generated image + + Returns: + str: Updated image filepath if regenerated, otherwise original filepath + """ + # Import image regeneration function here to avoid circular import + from .ai_blog_generator_utils import regenerate_blog_image + + st.subheader("🖼️ Featured Image") + image_container = st.container() + + # Display featured image + with image_container: + if generated_image_filepath: + st.image(generated_image_filepath, caption=blog_title or "Featured Image", use_column_width=True) + + # Add regenerate button + if st.button("🔄 Regenerate Image", key="regenerate_image"): + new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags) + if new_image_path: + return new_image_path + else: + st.info("No featured image was generated. Click below to generate one.") + if st.button("🖼️ Generate Image", key="generate_image"): + new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags) + if new_image_path: + return new_image_path + + return generated_image_filepath + + +def display_blog_content_and_audio(blog_markdown_str, saved_blog_to_file): + """ + Display the blog content and audio generation option. + + Args: + blog_markdown_str (str): Blog content + saved_blog_to_file (str): Path to the saved blog file + """ + # Display blog content + st.markdown("## Content") + st.markdown(blog_markdown_str) + + # Show file save information if available + if saved_blog_to_file: + st.success(f"✅ Blog saved to: {saved_blog_to_file}") + + # Add the audio generation button + st.markdown("---") + audio_col1, audio_col2 = st.columns([1, 3]) + with audio_col1: + generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True) + + with audio_col2: + if generate_audio_button: + generate_audio_version(blog_markdown_str) + + +def display_final_metadata_table(metadata, update_progress): + """ + Display the final metadata table and options. + + Args: + metadata (dict): Blog metadata + update_progress (function): Function to update progress + """ + update_progress(4, 5, "Preparing final blog presentation") + + st.markdown("---") + # Display metadata in a collapsible expander to save space + with st.expander("🏷️ Metadata", expanded=True): + st.table({ + "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"], + "Value": [ + metadata["blog_title"], + metadata["blog_meta_desc"], + metadata["blog_tags"], + metadata["blog_categories"], + metadata["blog_hashtags"], + metadata["blog_slug"] + ] + }) + + # Add buttons in columns for refining metadata + create_metadata_refinement_ui() + + # Add a row for structured data with a "Generate Rich Snippet" button + st.markdown("---") + st.markdown("### Get Structured Data") + + # Add structured data UI + create_structured_data_ui() + + # Create snippet generation dialog if button is clicked + if st.session_state.get("show_snippet_dialog", False): + display_structured_data_dialog(metadata["blog_title"], metadata["blog_tags"]) + + +def display_structured_data_dialog(blog_title, blog_tags): + """ + Display the structured data generation dialog. + + Args: + blog_title (str): Blog title + blog_tags (list): Blog tags + """ + with st.expander("Structured Data Generation Tool", expanded=True): + st.subheader("Generate Structured Data (Rich Snippets)") + + # Close button at the top + if st.button("Close", key="close_structured_data"): + st.session_state.show_snippet_dialog = False + st.rerun() + + # Simplified blog URL input + blog_url = st.text_input( + "Blog URL:", + placeholder="https://yourblog.com/your-article", + help="Enter the URL where this blog will be published" + ) + + # Auto-fill content type to "Article" since we're working with a blog + content_type = "Article" + st.info(f"Content Type: {content_type} (Auto-selected for blog content)") + + # Form for additional article details + with st.form(key="structured_data_form"): + st.markdown("#### Article Details") + + # Pre-fill with blog title and other metadata + article_title = st.text_input("Headline:", value=blog_title if blog_title else "") + article_author = st.text_input("Author:", value="") + article_date = st.date_input("Date Published:", value=datetime.now()) + article_keywords = st.text_input("Keywords:", value=blog_tags if blog_tags else "") + + submit_structured_data = st.form_submit_button("Generate JSON-LD") + + if submit_structured_data: + if not blog_url: + st.error("Please enter a blog URL to generate structured data.") + else: + # Create details dictionary + details = { + "Headline": article_title, + "Author": article_author, + "Date Published": article_date, + "Keywords": article_keywords + } + + # Call the imported ai_structured_data function or recreate its functionality + with st.spinner("Generating structured data..."): + # Import and use the function from the module directly + from ...ai_seo_tools.seo_structured_data import generate_json_data + + # Generate the structured data + structured_data = generate_json_data(content_type, details, blog_url) + + if structured_data: + st.success("✅ Structured data generated successfully!") + st.markdown("### Generated JSON-LD Code") + st.code(structured_data, language="json") + + # Download button + st.download_button( + label="📥 Download JSON-LD", + data=structured_data, + file_name=f"{content_type}_structured_data.json", + mime="application/json", + ) + + # Implementation instructions + with st.expander("How to Implement This Code"): + st.markdown(""" + ### Adding this JSON-LD to your website: + + 1. **Copy the generated JSON-LD code** above + + 2. **Add it to the `` section of your HTML** like this: + ```html + + ``` + + 3. **Verify the implementation** using Google's Rich Results Test tool: + [https://search.google.com/test/rich-results](https://search.google.com/test/rich-results) + + 4. **Monitor your search appearance** in Google Search Console + """) + else: + st.error("Failed to generate structured data. Please check your inputs and try again.") + + +def display_title_refinement_dialog(blog_title, blog_tags): + """ + Display a dialog for refining the blog title. + + Args: + blog_title (str): Current blog title + blog_tags (list): Blog tags for context + """ + with st.expander("Blog Title Refinement Tool", expanded=True): + st.subheader("Generate Better Blog Titles") + + # Form for title generation + with st.form(key="title_generation_form"): + st.markdown("#### Title Generation Parameters") + + # Pre-fill with blog tags if available + keywords = st.text_input("Target Keywords:", + value=blog_tags if blog_tags else "", + help="Enter primary keywords to target in the title") + + blog_type = st.selectbox( + "Blog Type:", + ["How-to Guide", "Tutorial", "List Post", "Informational", "Case Study", "Opinion Piece", "Review"], + index=0, + help="Select the type of blog you're creating" + ) + + search_intent = st.selectbox( + "Search Intent:", + ["Informational", "Commercial", "Navigational", "Transactional"], + index=0, + help="Select the primary search intent your title should address" + ) + + language = st.selectbox( + "Language:", + ["English", "Spanish", "French", "German", "Italian"], + index=0 + ) + + submit_title = st.form_submit_button("Generate Title Suggestions") + + if submit_title: + with st.spinner("Generating title suggestions..."): + # Import and use the function from the module + from ...ai_seo_tools.content_title_generator import generate_blog_titles + + # Generate the titles + title_suggestions = generate_blog_titles( + target_keywords=keywords, + blog_type=blog_type, + search_intent=search_intent, + language=language + ) + + if title_suggestions: + st.success("✅ Generated title suggestions!") + + # Display each title with an option to select it + st.markdown("### Select a Title or Modify") + + selected_title = st.text_input( + "Selected or Modified Title:", + value=blog_title if blog_title else (title_suggestions[0] if title_suggestions else ""), + help="Select one of the suggested titles or modify it to your preference" + ) + + if st.button("Confirm Title"): + st.session_state.blog_title = selected_title + st.session_state.show_title_dialog = False + st.success(f"Title updated to: {selected_title}") + st.rerun() + + # Display all suggestions + for i, title in enumerate(title_suggestions): + st.markdown(f"**Option {i+1}:** {title}") + else: + st.error("Failed to generate title suggestions. Please try different parameters.") + + +def display_meta_description_dialog(blog_meta_desc, blog_tags): + """ + Display a dialog for refining the meta description. + + Args: + blog_meta_desc (str): Current meta description + blog_tags (list): Blog tags for context + """ + with st.expander("Meta Description Refinement Tool", expanded=True): + st.subheader("Generate Optimized Meta Descriptions") + + # Form for meta description generation + with st.form(key="meta_desc_generation_form"): + st.markdown("#### Meta Description Parameters") + + # Pre-fill with blog tags if available + keywords = st.text_input("Target Keywords:", + value=blog_tags if blog_tags else "", + help="Enter primary keywords to target in the meta description") + + tone = st.selectbox( + "Tone:", + ["Informative", "Engaging", "Professional", "Conversational", "Humorous", "Urgent"], + index=0, + help="Select the tone for your meta description" + ) + + search_intent = st.selectbox( + "Search Intent:", + ["Informational", "Commercial", "Navigational", "Transactional"], + index=0, + help="Select the primary search intent your meta description should address" + ) + + language = st.selectbox( + "Language:", + ["English", "Spanish", "French", "German", "Italian"], + index=0 + ) + + submit_meta = st.form_submit_button("Generate Meta Description Suggestions") + + if submit_meta: + with st.spinner("Generating meta description suggestions..."): + # Import and use the function from the module + from ...ai_seo_tools.meta_desc_generator import generate_blog_metadesc + + # Generate the meta descriptions + meta_suggestions = generate_blog_metadesc( + target_keywords=keywords, + tone=tone, + search_intent=search_intent, + language=language + ) + + if meta_suggestions: + st.success("✅ Generated meta description suggestions!") + + # Display each meta description with an option to select it + st.markdown("### Select a Meta Description or Modify") + + selected_meta = st.text_area( + "Selected or Modified Meta Description:", + value=blog_meta_desc if blog_meta_desc else (meta_suggestions[0] if meta_suggestions else ""), + height=100, + help="Select one of the suggested meta descriptions or modify it to your preference" + ) + + if st.button("Confirm Meta Description"): + st.session_state.blog_meta_desc = selected_meta + st.session_state.show_meta_dialog = False + st.success(f"Meta description updated!") + st.rerun() + + # Display all suggestions + for i, meta in enumerate(meta_suggestions): + st.markdown(f"**Option {i+1}:** {meta}") + else: + st.error("Failed to generate meta description suggestions. Please try different parameters.") + + +def write_blog_from_keywords(search_keywords, url=None, search_params=None, blog_params=None): + """ + This function will take a blog Topic to first generate sections for it + and then generate content for each section. + + Args: + search_keywords (str): Keywords to research and write about + url (str, optional): Optional URL to use as a source + search_params (dict, optional): Dictionary of search parameters including: + - max_results: Maximum number of search results (default: 10) + - search_depth: "basic" or "advanced" search depth (default: "basic") + - include_domains: List of domains to prioritize in search + - time_range: Time range for results (default: "year") + blog_params (dict, optional): Dictionary of blog content characteristics including: + - blog_length: Target word count (default: 2000) + - blog_tone: Tone of the content (default: "Professional") + - blog_demographic: Target audience (default: "Professional") + - blog_type: Type of blog post (default: "Informational") + - blog_language: Language for the blog (default: "English") + - blog_output_format: Format for the blog (default: "markdown") + """ + # Check if we need to display any dialog boxes first + if st.session_state.get("show_title_dialog") and "blog_title" in st.session_state: + display_title_refinement_dialog(st.session_state.blog_title, None) + return None + + if st.session_state.get("show_meta_dialog") and "blog_meta_desc" in st.session_state: + display_meta_description_dialog(st.session_state.blog_meta_desc, None) + return None + + if st.session_state.get("show_snippet_dialog"): + # Get blog title and tags to pass to the dialog + blog_title = st.session_state.get("blog_title", "") + blog_tags = st.session_state.get("blog_tags", "") + display_structured_data_dialog(blog_title, blog_tags) + return None + + # Initialize parameters with defaults + search_params, blog_params = initialize_parameters(search_params, blog_params) + + # Set up progress tracking + final_content_placeholder, progress_placeholder, progress_bar, status_text, update_progress = setup_progress_tracking() + + # STEP 1: Research phase + google_search_result, tavily_search_result, google_search_success, tavily_search_success, example_blog_titles = perform_research_phase( + search_keywords, search_params, update_progress + ) + + # Check if both searches failed - if so, stop the process + if not google_search_success and not tavily_search_success: + update_progress(5, 5, "Research failed") + progress_placeholder.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.") + st.warning("Please check your API keys in the environment settings and try again.") + st.stop() + return None + + # STEP 2: Content generation phase + blog_markdown_str = generate_content_phase( + search_keywords, google_search_result, tavily_search_result, + google_search_success, tavily_search_success, blog_params, update_progress + ) + + if not blog_markdown_str: + update_progress(5, 5, "Content generation failed") + progress_placeholder.error("⛔ Failed to generate blog content from research data.") + st.stop() + return None + + # STEP 3: Metadata & enhancement phase + metadata, generated_image_filepath, saved_blog_to_file = generate_metadata_and_image( + blog_markdown_str, search_keywords, None, update_progress + ) + + # Display image with regeneration option + updated_image_filepath = display_featured_image( + metadata["blog_title"], metadata["blog_meta_desc"], + blog_markdown_str, metadata["blog_tags"], generated_image_filepath + ) + + if updated_image_filepath != generated_image_filepath: + generated_image_filepath = updated_image_filepath + st.rerun() # Refresh the page to show the new image + + # Display blog content and audio option + display_blog_content_and_audio(blog_markdown_str, saved_blog_to_file) + + # STEP 4: Final presentation + with final_content_placeholder.container(): + display_final_metadata_table(metadata, update_progress) + + # If there's a button click to generate a structured data snippet, handle it + if st.session_state.get("show_snippet_dialog", False): + display_structured_data_dialog(metadata["blog_title"], metadata["blog_tags"]) + + # Final progress update + update_progress(5, 5, "Blog generation complete!") + + # Replace progress bar with success message + progress_placeholder.success("✅ Blog generation process completed successfully!") + + return blog_markdown_str diff --git a/lib/ai_writers/keywords_to_blog_streamlit.py b/lib/ai_writers/keywords_to_blog_streamlit.py deleted file mode 100644 index 35b53fd1..00000000 --- a/lib/ai_writers/keywords_to_blog_streamlit.py +++ /dev/null @@ -1,1397 +0,0 @@ -import sys -import os -import asyncio -from textwrap import dedent -from pathlib import Path -from datetime import datetime -import streamlit as st -from gtts import gTTS -import base64 -from dotenv import load_dotenv -import time - -# Load environment variables -load_dotenv(Path('../../.env')) -# Logger setup -from loguru import logger -logger.remove() -logger.add(sys.stdout, - colorize=True, - format="{level}|{file}:{line}:{function}| {message}") - -# Import other necessary modules -from ..ai_web_researcher.gpt_online_researcher import ( - do_google_serp_search as gpt_do_google_serp_search, - do_tavily_ai_search as gpt_do_tavily_ai_search, - do_metaphor_ai_research, do_google_pytrends_analysis) -from .blog_from_google_serp import write_blog_google_serp, blog_with_research -from ..blog_metadata.get_blog_metadata import blog_metadata -from ..blog_postprocessing.save_blog_to_file import save_blog_to_file -from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image -from ..ai_seo_tools.content_title_generator import generate_blog_titles -from ..ai_seo_tools.meta_desc_generator import generate_blog_metadesc -from ..ai_seo_tools.seo_structured_data import ai_structured_data - - -def initialize_parameters(search_params=None, blog_params=None): - """ - Initialize and validate search and blog parameters with defaults. - - Args: - search_params (dict, optional): Search parameters - blog_params (dict, optional): Blog parameters - - Returns: - tuple: (search_params, blog_params) with defaults applied - """ - # Initialize search params if not provided - if search_params is None: - search_params = {} - - # Initialize blog params if not provided - if blog_params is None: - blog_params = {} - - # Provide default values only for missing keys - # This ensures we don't override values that were intentionally set to 0 or other falsy values - if "max_results" not in search_params: - search_params["max_results"] = 10 - if "search_depth" not in search_params: - search_params["search_depth"] = "basic" - if "time_range" not in search_params: - search_params["time_range"] = "year" - if "include_domains" not in search_params: - search_params["include_domains"] = [] - - # Provide default values only for missing blog parameter keys - if "blog_length" not in blog_params: - blog_params["blog_length"] = 2000 - if "blog_tone" not in blog_params: - blog_params["blog_tone"] = "Professional" - if "blog_demographic" not in blog_params: - blog_params["blog_demographic"] = "Professional" - if "blog_type" not in blog_params: - blog_params["blog_type"] = "Informational" - if "blog_language" not in blog_params: - blog_params["blog_language"] = "English" - if "blog_output_format" not in blog_params: - blog_params["blog_output_format"] = "markdown" - - # Log the parameters for debugging - logger.info(f"Using search parameters: {search_params}") - logger.info(f"Using blog parameters: {blog_params}") - - return search_params, blog_params - - -def perform_google_search(search_keywords, search_params, status, status_container, progress_bar): - """ - Perform Google SERP search for the given keywords. - - Args: - search_keywords (str): Keywords to search for - search_params (dict): Search parameters - status: Streamlit status object - status_container: Streamlit container for status messages - progress_bar: Streamlit progress bar - - Returns: - tuple: (google_search_result, g_titles, success_flag) - """ - def update_progress(message, progress=None, level="info"): - """Helper function to update progress in Streamlit UI""" - if progress is not None: - progress_bar.progress(progress) - - if level == "error": - status_container.error(f"🚫 {message}") - elif level == "warning": - status_container.warning(f"⚠️ {message}") - elif level == "success": - status_container.success(f"✅ {message}") - else: - status_container.info(f"🔄 {message}") - logger.debug(f"Progress update [{level}]: {message}") - - try: - # Update the function call to include the required parameters and search_params - status.update(label=f"Starting Google SERP search for: {search_keywords}") - - # Add search params to the Google SERP search - google_search_params = { - "max_results": search_params.get("max_results", 10) - } - - # Include domains if provided - if search_params.get("include_domains"): - google_search_params["include_domains"] = search_params.get("include_domains") - - google_search_result = do_google_serp_search( - search_keywords, - status_container=status_container, - update_progress=update_progress, - **google_search_params - ) - - if google_search_result and google_search_result.get('titles') and len(google_search_result.get('titles', [])) > 0: - status.update(label=f"✅ Finished with Google web for Search: {search_keywords}") - g_titles = google_search_result.get('titles', []) - return google_search_result, g_titles, True - else: - # Check if there's an error message in the result - if google_search_result and 'summary' in google_search_result and 'Error' in google_search_result['summary']: - error_msg = google_search_result['summary'] - status.update(label=f"❌ Google search failed: {error_msg}", state="error") - st.error(f"Google SERP search failed: {error_msg}") - else: - status.update(label="❌ Failed to get Google SERP results. No valid data returned.", state="error") - st.error("Google SERP search failed to return valid results.") - return google_search_result, [], False - except Exception as err: - status.update(label=f"❌ Google search error: {str(err)}", state="error") - st.error(f"Google web research failed: {err}") - logger.error(f"Failed in Google web research: {err}") - return None, [], False - - -def perform_tavily_search(search_keywords, search_params, status): - """ - Perform Tavily AI search for the given keywords. - - Args: - search_keywords (str): Keywords to search for - search_params (dict): Search parameters - status: Streamlit status object - - Returns: - tuple: (tavily_search_result, success_flag) - """ - try: - status.update(label=f"🔍 Starting Tavily AI research: {search_keywords}") - - # Pass the search parameters to Tavily - tavily_result_tuple = do_tavily_ai_search( - search_keywords, - max_results=search_params.get("max_results", 10), - search_depth=search_params.get("search_depth", "basic"), - include_domains=search_params.get("include_domains", []), - time_range=search_params.get("time_range", "year") - ) - - if tavily_result_tuple and len(tavily_result_tuple) == 3: - tavily_search_result, t_titles, t_answer = tavily_result_tuple - # If we have either titles or an answer, consider it a success - if (t_titles and len(t_titles) > 0) or (t_answer and len(t_answer) > 10): - status.update(label=f"✅ Finished Tavily AI Search on: {search_keywords}", state="complete") - return tavily_search_result, True - else: - status.update(label="❌ Tavily search returned empty results", state="error") - st.warning("Tavily search didn't find relevant information.") - return tavily_search_result, False - else: - status.update(label="❌ Tavily search returned incomplete results", state="error") - st.error("Tavily search failed to return valid results.") - return None, False - - except Exception as err: - status.update(label=f"❌ Tavily search error: {str(err)}", state="error") - st.error(f"Failed in Tavily web research: {err}") - logger.error(f"Failed in Tavily web research: {err}") - return None, False - - -def generate_blog_content(search_keywords, google_search_result, tavily_search_result, - google_search_success, tavily_search_success, blog_params, status): - """ - Generate blog content using either Google or Tavily search results. - - Args: - search_keywords (str): Search keywords - google_search_result: Results from Google search - tavily_search_result: Results from Tavily search - google_search_success (bool): Whether Google search was successful - tavily_search_success (bool): Whether Tavily search was successful - blog_params (dict): Blog parameters - status: Streamlit status object - - Returns: - str: Generated blog content or None if generation failed - """ - # Check if both searches failed - if so, stop the process - if not google_search_success and not tavily_search_success: - st.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.") - st.warning("Please check your API keys in the environment settings and try again.") - return None - - # Try Google results first if available - if google_search_success and 'results' in google_search_result: - try: - status.update(label=f"✏️ Writing blog from Google Search results...") - # Pass blog parameters to the blog writing function - blog_style_info = f""" - Length: {blog_params.get('blog_length')} words - Tone: {blog_params.get('blog_tone')} - Target Audience: {blog_params.get('blog_demographic')} - Blog Type: {blog_params.get('blog_type')} - Language: {blog_params.get('blog_language')} - """ - status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...") - blog_markdown_str = write_blog_google_serp(search_keywords, google_search_result['results'], blog_params=blog_params) - status.update(label="✅ Generated content from Google search results", state="complete") - return blog_markdown_str - except Exception as err: - status.update(label=f"❌ Failed to generate content from Google results: {str(err)}", state="error") - st.error(f"Failed to generate content from Google results: {err}") - logger.error(f"Failed to process Google search results: {err}") - - # If Google failed or had no results, try Tavily - if tavily_search_success and tavily_search_result: - try: - status.update(label=f"✏️ Writing blog from Tavily search results...") - status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...") - blog_markdown_str = write_blog_google_serp(search_keywords, tavily_search_result, blog_params=blog_params) - status.update(label="✅ Generated content from Tavily search results", state="complete") - return blog_markdown_str - except Exception as err: - status.update(label=f"❌ Failed to generate content from Tavily results: {str(err)}", state="error") - st.error(f"Failed to generate content from Tavily results: {err}") - logger.error(f"Failed to process Tavily search results: {err}") - - # If we still don't have content, show error - st.error("⛔ Failed to generate any blog content from the research results.") - return None - - -def generate_blog_metadata(blog_markdown_str, search_keywords, status): - """ - Generate metadata for the blog content. - - Args: - blog_markdown_str (str): Blog content - search_keywords (str): Original search keywords - status: Streamlit status object - - Returns: - tuple: (blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug) - """ - status.update(label="🔍 Generating title, meta description, tags, categories, hashtags, and slug...") - try: - # Get all 6 metadata values from blog_metadata - blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = asyncio.run(blog_metadata(blog_markdown_str)) - status.update(label="✅ Generated blog metadata successfully") - return blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug - except Exception as err: - st.error(f"Failed to get blog metadata: {err}") - logger.error(f"Failed to get blog metadata: {err}") - status.update(label="❌ Failed to get blog metadata", state="error") - return None, None, None, None, None, None - - -def generate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags=None): - """ - Generate a featured image for the blog. - - Args: - blog_title (str): Blog title - blog_meta_desc (str): Blog meta description - blog_markdown_str (str): Blog content - status: Streamlit status object - blog_tags (list, optional): Blog tags to use for image prompt enhancement - - Returns: - str: Path to the generated image or None if generation failed - """ - try: - status.update(label="🖼️ Generating featured image for blog...") - - # Create a better prompt for image generation - if blog_title and blog_meta_desc: - # If we have both title and description, use them - text_to_image = f"{blog_title}: {blog_meta_desc}" - elif blog_title: - # If we only have title, use it - text_to_image = blog_title - elif blog_meta_desc: - # If we only have description, use it - text_to_image = blog_meta_desc - else: - # Fallback to first 200 chars of content - text_to_image = blog_markdown_str[:200] - - # Ensure the prompt is of reasonable length - if len(text_to_image) > 300: - text_to_image = text_to_image[:300] - - # Log the prompt being used - logger.info(f"Generating image with prompt: {text_to_image}") - status.update(label=f"🖼️ Creating image with prompt: \"{text_to_image[:50]}...\"") - - # Extract blog tags if available - blog_tags_list = blog_tags if isinstance(blog_tags, list) else [] - - # Attempt image generation with all available parameters - generated_image_filepath = generate_image( - user_prompt=text_to_image, - title=blog_title, - description=blog_meta_desc, - tags=blog_tags_list, - content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads - ) - - # If first attempt failed, try with a simplified prompt - if not generated_image_filepath: - logger.warning("First image generation attempt failed, trying with simplified prompt") - status.update(label="⚠️ First image attempt failed, trying again with simplified prompt...") - - # Create a simpler prompt - simplified_prompt = " ".join(text_to_image.split()[:10]) - generated_image_filepath = generate_image( - user_prompt=simplified_prompt, - title=blog_title, - description=blog_meta_desc, - tags=blog_tags_list, - content=blog_markdown_str[:1000] # Use even shorter content for the retry - ) - - if generated_image_filepath: - status.update(label="✅ Successfully generated featured image") - return generated_image_filepath - else: - status.update(label="❌ Image generation failed - no image created", state="error") - return None - - except Exception as err: - st.warning(f"Failed in Image generation: {err}") - logger.error(f"Failed in Image generation: {err}") - status.update(label="❌ Image generation failed - no image created", state="error") - return None - - -def regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags=None): - """ - Regenerate a blog image on demand. - - Args: - blog_title (str): Blog title - blog_meta_desc (str): Blog meta description - blog_markdown_str (str): Blog content - blog_tags (list, optional): Blog tags to use for image prompt enhancement - - Returns: - str: Path to the generated image or None if generation failed - """ - with st.status("Regenerating image...", expanded=True) as status: - try: - # Use keywords from title or description - if blog_title: - keywords = " ".join(blog_title.split()[:6]) - prompt = f"Blog illustration for: {keywords}" - elif blog_meta_desc: - keywords = " ".join(blog_meta_desc.split()[:6]) - prompt = f"Blog illustration for: {keywords}" - else: - keywords = blog_markdown_str.split()[:50] - prompt = f"Blog illustration based on: {' '.join(keywords[:6])}" - - status.update(label=f"🖼️ Generating new image with prompt: \"{prompt}\"") - - # Extract any tags if available - will be passed as empty list otherwise - blog_tags_list = blog_tags if isinstance(blog_tags, list) else [] - - # Generate the image with all parameters - generated_image_filepath = generate_image( - user_prompt=prompt, - title=blog_title, - description=blog_meta_desc, - tags=blog_tags_list, - content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads - ) - - if generated_image_filepath: - status.update(label="✅ Successfully generated new image", state="complete") - return generated_image_filepath - else: - status.update(label="❌ Image regeneration failed", state="error") - return None - - except Exception as err: - st.error(f"Failed to regenerate image: {err}") - logger.error(f"Image regeneration error: {err}") - status.update(label="❌ Image regeneration failed", state="error") - return None - - -def save_blog_content(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath, status, blog_hashtags=None, blog_slug=None): - """ - Save the blog content to a file. - - Args: - blog_markdown_str (str): Blog content - blog_title (str): Blog title - blog_meta_desc (str): Blog meta description - blog_tags (list): Blog tags - blog_categories (list): Blog categories - generated_image_filepath (str): Path to the generated image - status: Streamlit status object - blog_hashtags (str, optional): Social media hashtags - blog_slug (str, optional): SEO-friendly URL slug - - Returns: - str: Path to the saved file or None if saving failed - """ - try: - status.update(label="💾 Saving blog content to file...") - saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc, - blog_tags, blog_categories, generated_image_filepath) - status.update(label=f"✅ Saved the content to: {saved_blog_to_file}") - return saved_blog_to_file - except Exception as err: - st.error(f"Failed to save blog to file: {err}") - logger.error(f"Failed to save blog to file: {err}") - status.update(label="❌ Failed to save blog to file", state="error") - return None - - -def generate_audio_version(blog_markdown_str, status=None): - """ - Generate an audio version of the blog content. - - Args: - blog_markdown_str (str): Blog content - status: Streamlit status object (optional) - - Returns: - bool: True if audio generation was successful, False otherwise - """ - try: - if status: - status.update(label="🔊 Generating audio version of the blog...") - else: - st.info("🔊 Generating audio version...") - - # Only generate audio for reasonable-sized blogs (to avoid errors with very large text) - if blog_markdown_str and len(blog_markdown_str) < 50000: # Max ~50KB of text - tts = gTTS(text=blog_markdown_str[:40000], lang='en', slow=False) # Use first 40K chars to be safe - tts.save("delete_me.mp3") - st.audio("delete_me.mp3") - st.download_button( - label="📥 Download Audio File", - data=open("delete_me.mp3", "rb").read(), - file_name="blog_audio.mp3", - mime="audio/mp3" - ) - if status: - status.update(label="✅ Audio version generated successfully", state="complete") - else: - st.success("✅ Audio version generated successfully") - return True - else: - st.warning("Blog content too large for audio generation") - if status: - status.update(label="⚠️ Blog content too large for audio generation", state="complete") - return False - except Exception as err: - st.warning(f"Failed to generate audio version: {err}") - logger.error(f"Failed to generate audio version: {err}") - if status: - status.update(label="❌ Failed to generate audio version", state="error") - return False - - -def write_blog_from_keywords(search_keywords, url=None, search_params=None, blog_params=None): - """ - This function will take a blog Topic to first generate sections for it - and then generate content for each section. - - Args: - search_keywords (str): Keywords to research and write about - url (str, optional): Optional URL to use as a source - search_params (dict, optional): Dictionary of search parameters including: - - max_results: Maximum number of search results (default: 10) - - search_depth: "basic" or "advanced" search depth (default: "basic") - - include_domains: List of domains to prioritize in search - - time_range: Time range for results (default: "year") - blog_params (dict, optional): Dictionary of blog content characteristics including: - - blog_length: Target word count (default: 2000) - - blog_tone: Tone of the content (default: "Professional") - - blog_demographic: Target audience (default: "Professional") - - blog_type: Type of blog post (default: "Informational") - - blog_language: Language for the blog (default: "English") - - blog_output_format: Format for the blog (default: "markdown") - """ - # Initialize parameters with defaults - search_params, blog_params = initialize_parameters(search_params, blog_params) - - # Create a placeholder for the final blog content - final_content_placeholder = st.empty() - - # Create progress tracking - progress_placeholder = st.empty() - with progress_placeholder.container(): - progress_bar = st.progress(0) - status_text = st.empty() - - def update_progress(step, total_steps, message): - """Update the progress bar and status message""" - progress_value = min(step / total_steps, 1.0) - progress_bar.progress(progress_value) - status_text.info(f"Step {step}/{total_steps}: {message}") - - # Set up processing variables - blog_markdown_str = None - example_blog_titles = [] - google_search_success = False - tavily_search_success = False - blog_title = None - blog_meta_desc = None - blog_tags = None - blog_categories = None - generated_image_filepath = None - saved_blog_to_file = None - - # STEP 1: Research phase - update_progress(1, 5, f"Starting web research on '{search_keywords}'") - logger.info(f"Researching and Writing Blog on keywords: {search_keywords}") - - # Create a section header for the research phase - st.subheader("🔍 Web Research Progress") - - # Use a container instead of an expander - research_container = st.container() - with research_container: - # Create a status element for research updates - with st.status("Web research in progress...", expanded=True) as status: - status.update(label=f"📊 Performing web research on: {search_keywords}") - - # Create status container and progress tracking for Google SERP - status_container = st.empty() - research_progress = st.progress(0) - - # Google Search - status.update(label="🔍 Performing Google search...") - google_search_result, g_titles, google_search_success = perform_google_search( - search_keywords, search_params, status, status_container, research_progress - ) - if g_titles: - example_blog_titles.append(g_titles) - status.update(label=f"✅ Google search complete - found {len(g_titles)} relevant resources") - else: - status.update(label="⚠️ Google search yielded limited results") - - # Tavily Search - status.update(label="🔍 Performing Tavily AI search...") - tavily_search_result, tavily_search_success = perform_tavily_search( - search_keywords, search_params, status - ) - - if tavily_search_success: - status.update(label="✅ Tavily AI search complete", state="complete") - elif google_search_success: - status.update(label="⚠️ Tavily search had issues, but Google search was successful") - else: - status.update(label="❌ Both search methods encountered issues", state="error") - - # Clear the progress indicators - status_container.empty() - research_progress.empty() - - # Check if both searches failed - if so, stop the process - if not google_search_success and not tavily_search_success: - update_progress(5, 5, "Research failed") - progress_placeholder.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.") - st.warning("Please check your API keys in the environment settings and try again.") - st.stop() - return None - - # STEP 2: Content generation phase - update_progress(2, 5, "Generating blog content from research") - - # Create a section header for the content generation phase - st.subheader("✍️ Content Generation Progress") - - # Use a container instead of an expander - content_container = st.container() - with content_container: - # Create a status element for content generation updates - with st.status("Content generation in progress...", expanded=True) as status: - if google_search_success: - source = "Google search results" - else: - source = "Tavily AI research" - - status.update(label=f"📝 Creating {blog_params.get('blog_tone')} {blog_params.get('blog_type')} content for {blog_params.get('blog_demographic')} audience...") - - blog_markdown_str = generate_blog_content( - search_keywords, google_search_result, tavily_search_result, - google_search_success, tavily_search_success, blog_params, status - ) - - if blog_markdown_str: - status.update(label=f"✅ Successfully generated ~{len(blog_markdown_str.split())} words of content using {source}", state="complete") - else: - status.update(label="❌ Content generation failed", state="error") - update_progress(5, 5, "Content generation failed") - progress_placeholder.error("⛔ Failed to generate blog content from research data.") - st.stop() - return None - - # STEP 3: Metadata & enhancement phase - update_progress(3, 5, "Generating SEO metadata and enhancements") - - # Create a section header for the enhancement phase - st.subheader("🔍 SEO & Enhancement Progress") - - # Use a container instead of an expander - enhancement_container = st.container() - with enhancement_container: - # Create a status element for enhancement updates - with st.status("Enhancing content...", expanded=True) as status: - # Generate metadata - status.update(label="🏷️ Generating SEO metadata (title, description, tags)...") - blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata( - blog_markdown_str, search_keywords, status - ) - - # Check if there are updated values in session state - if 'blog_title' in st.session_state: - blog_title = st.session_state.blog_title - status.update(label=f"✅ Using refined title: \"{blog_title}\"") - - if 'blog_meta_desc' in st.session_state: - blog_meta_desc = st.session_state.blog_meta_desc - status.update(label=f"✅ Using refined meta description") - - if blog_title and blog_meta_desc: - status.update(label=f"✅ Generated metadata: \"{blog_title}\"") - - # Generate featured image - status.update(label="🖼️ Creating featured image...") - generated_image_filepath = generate_blog_image( - blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags - ) - - # Save blog content to file - status.update(label="💾 Saving blog content...") - saved_blog_to_file = save_blog_content( - blog_markdown_str, blog_title, blog_meta_desc, blog_tags, - blog_categories, generated_image_filepath, status, blog_hashtags, blog_slug - ) - - status.update(label="✅ Content enhancement complete", state="complete") - else: - status.update(label="⚠️ Metadata generation had issues, using simplified format", state="warning") - - # Add buttons in columns for refining metadata - col1, col2 = st.columns(2) - with col1: - refine_title_button = st.button("🔄 Refine Blog Title", use_container_width=True) - with col2: - refine_meta_button = st.button("🔄 Refine Meta Description", use_container_width=True) - - # Add a row for structured data - st.markdown("---") - structured_data_col1, structured_data_col2 = st.columns([3, 1]) - - with structured_data_col1: - # Educational popover explaining why rich snippets are important - with st.expander("ℹ️ Why Rich Snippets Are Important for SEO"): - st.markdown(""" - ### Rich Snippets: Boosting Your SEO and Click-Through Rates - - **What are Rich Snippets?** - - Rich snippets are enhanced search results that display additional information directly in search engine results pages (SERPs). They're created using structured data markup (JSON-LD) that helps search engines understand your content better. - - **Why are they important?** - - 1. **Increased Visibility**: Rich snippets stand out in search results with stars, images, and additional information - - 2. **Higher Click-Through Rates (CTR)**: Studies show rich snippets can increase CTR by 30-150% - - 3. **Improved SEO**: They help search engines understand your content better, potentially improving rankings - - 4. **Enhanced User Experience**: Users can see key information before clicking, leading to more qualified traffic - - 5. **Mobile-Friendly**: Rich snippets are especially effective on mobile searches - - **Common types of rich snippets include:** - - Articles/Blogs (with author, date, image) - - Products (with ratings, price, availability) - - Recipes (with cooking time, ratings, calories) - - Events (with date, location, ticket info) - - Local Business (with address, hours, ratings) - - Adding structured data to your content is a powerful SEO technique that requires minimal effort but provides significant benefits. - """) - - with structured_data_col2: - # Button to generate rich snippet - generate_snippet_button = st.button("📊 Generate Rich Snippet", use_container_width=True) - - # Dialog for generating structured data - if generate_snippet_button: - with st.expander("Structured Data Generation Tool", expanded=True): - st.subheader("Generate Structured Data (Rich Snippets)") - - # Simplified blog URL input - blog_url = st.text_input( - "Blog URL:", - placeholder="https://yourblog.com/your-article", - help="Enter the URL where this blog will be published" - ) - - # Auto-fill content type to "Article" since we're working with a blog - content_type = "Article" - st.info(f"Content Type: {content_type} (Auto-selected for blog content)") - - # Create details dictionary with blog metadata - today = datetime.now().strftime("%Y-%m-%d") - - # Form for additional article details - with st.form(key="structured_data_form"): - st.markdown("#### Article Details") - - # Pre-fill with blog title and other metadata - article_title = st.text_input("Headline:", value=blog_title if blog_title else "") - article_author = st.text_input("Author:", value="") - article_date = st.date_input("Date Published:", value=datetime.now()) - article_keywords = st.text_input("Keywords:", value=blog_tags if blog_tags else "") - - submit_structured_data = st.form_submit_button("Generate JSON-LD") - - if submit_structured_data: - if not blog_url: - st.error("Please enter a blog URL to generate structured data.") - else: - # Create details dictionary - details = { - "Headline": article_title, - "Author": article_author, - "Date Published": article_date, - "Keywords": article_keywords - } - - # Call the imported ai_structured_data function or recreate its functionality - with st.spinner("Generating structured data..."): - # Import and use the function from the module directly - from ..ai_seo_tools.seo_structured_data import generate_json_data - - # Generate the structured data - structured_data = generate_json_data(content_type, details, blog_url) - - if structured_data: - st.success("✅ Structured data generated successfully!") - st.markdown("### Generated JSON-LD Code") - st.code(structured_data, language="json") - - # Download button - st.download_button( - label="📥 Download JSON-LD", - data=structured_data, - file_name=f"{content_type}_structured_data.json", - mime="application/json", - ) - - # Implementation instructions - with st.expander("How to Implement This Code"): - st.markdown(""" - ### Adding this JSON-LD to your website: - - 1. **Copy the generated JSON-LD code** above - - 2. **Add it to the `` section of your HTML** like this: - ```html - - ``` - - 3. **Verify the implementation** using Google's Rich Results Test tool: - [https://search.google.com/test/rich-results](https://search.google.com/test/rich-results) - - 4. **Monitor your search appearance** in Google Search Console - """) - else: - st.error("Failed to generate structured data. Please check your inputs and try again.") - - # Image section with regeneration option - st.subheader("🖼️ Featured Image") - image_container = st.container() - - # Display featured image - with image_container: - if generated_image_filepath: - st.image(generated_image_filepath, caption=blog_title or "Featured Image", use_column_width=True) - - # Add regenerate button - if st.button("🔄 Regenerate Image", key="regenerate_image"): - new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags) - if new_image_path: - generated_image_filepath = new_image_path - st.rerun() # Refresh the page to show the new image - else: - st.info("No featured image was generated. Click below to generate one.") - if st.button("🖼️ Generate Image", key="generate_image"): - new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags) - if new_image_path: - generated_image_filepath = new_image_path - st.rerun() # Refresh the page to show the new image - - # Display blog content - st.markdown("## Content") - st.markdown(blog_markdown_str) - - # Show file save information if available - if saved_blog_to_file: - st.success(f"✅ Blog saved to: {saved_blog_to_file}") - - # Add the audio generation button - st.markdown("---") - audio_col1, audio_col2 = st.columns([1, 3]) - with audio_col1: - generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True) - - with audio_col2: - if generate_audio_button: - generate_audio_version(blog_markdown_str) - - # STEP 4: Final presentation - update_progress(4, 5, "Preparing final blog presentation") - - # Now display the final blog content - with final_content_placeholder.container(): - st.markdown("---") - # Display tabular data of metadata - st.subheader("🏷️ Metadata") - metadata_container = st.container() - with metadata_container: - st.table({ - "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"], - "Value": [blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug] - }) - - # Add buttons in columns for refining metadata - col1, col2 = st.columns(2) - with col1: - refine_title_button = st.button("🔄 Refine Blog Title", use_container_width=True) - with col2: - refine_meta_button = st.button("🔄 Refine Meta Description", use_container_width=True) - - # Add a row for structured data with a "Generate Rich Snippet" button - st.markdown("---") - st.markdown("### Get Structured Data") - - structured_data_col1, structured_data_col2 = st.columns([3, 1]) - - with structured_data_col1: - # Educational popover explaining why rich snippets are important - with st.expander("ℹ️ Why Rich Snippets Are Important for SEO"): - st.markdown(""" - ### Rich Snippets: Boosting Your SEO and Click-Through Rates - - **What are Rich Snippets?** - - Rich snippets are enhanced search results that display additional information directly in search engine results pages (SERPs). They're created using structured data markup (JSON-LD) that helps search engines understand your content better. - - **Why are they important?** - - 1. **Increased Visibility**: Rich snippets stand out in search results with stars, images, and additional information - - 2. **Higher Click-Through Rates (CTR)**: Studies show rich snippets can increase CTR by 30-150% - - 3. **Improved SEO**: They help search engines understand your content better, potentially improving rankings - - 4. **Enhanced User Experience**: Users can see key information before clicking, leading to more qualified traffic - - 5. **Mobile-Friendly**: Rich snippets are especially effective on mobile searches - - **Common types of rich snippets include:** - - Articles/Blogs (with author, date, image) - - Products (with ratings, price, availability) - - Recipes (with cooking time, ratings, calories) - - Events (with date, location, ticket info) - - Local Business (with address, hours, ratings) - - Adding structured data to your content is a powerful SEO technique that requires minimal effort but provides significant benefits. - """) - - with structured_data_col2: - # Button to generate rich snippet - generate_snippet_button = st.button("📊 Generate Rich Snippet", use_container_width=True) - - # Dialog for generating structured data - if generate_snippet_button: - with st.expander("Structured Data Generation Tool", expanded=True): - st.subheader("Generate Structured Data (Rich Snippets)") - - # Simplified blog URL input - blog_url = st.text_input( - "Blog URL:", - placeholder="https://yourblog.com/your-article", - help="Enter the URL where this blog will be published" - ) - - # Auto-fill content type to "Article" since we're working with a blog - content_type = "Article" - st.info(f"Content Type: {content_type} (Auto-selected for blog content)") - - # Form for additional article details - with st.form(key="structured_data_form"): - st.markdown("#### Article Details") - - # Pre-fill with blog title and other metadata - article_title = st.text_input("Headline:", value=blog_title if blog_title else "") - article_author = st.text_input("Author:", value="") - article_date = st.date_input("Date Published:", value=datetime.now()) - article_keywords = st.text_input("Keywords:", value=blog_tags if blog_tags else "") - - submit_structured_data = st.form_submit_button("Generate JSON-LD") - - if submit_structured_data: - if not blog_url: - st.error("Please enter a blog URL to generate structured data.") - else: - # Create details dictionary - details = { - "Headline": article_title, - "Author": article_author, - "Date Published": article_date, - "Keywords": article_keywords - } - - # Call the imported ai_structured_data function or recreate its functionality - with st.spinner("Generating structured data..."): - # Import and use the function from the module directly - from ..ai_seo_tools.seo_structured_data import generate_json_data - - # Generate the structured data - structured_data = generate_json_data(content_type, details, blog_url) - - if structured_data: - st.success("✅ Structured data generated successfully!") - st.markdown("### Generated JSON-LD Code") - st.code(structured_data, language="json") - - # Download button - st.download_button( - label="📥 Download JSON-LD", - data=structured_data, - file_name=f"{content_type}_structured_data.json", - mime="application/json", - ) - - # Implementation instructions - with st.expander("How to Implement This Code"): - st.markdown(""" - ### Adding this JSON-LD to your website: - - 1. **Copy the generated JSON-LD code** above - - 2. **Add it to the `` section of your HTML** like this: - ```html - - ``` - - 3. **Verify the implementation** using Google's Rich Results Test tool: - [https://search.google.com/test/rich-results](https://search.google.com/test/rich-results) - - 4. **Monitor your search appearance** in Google Search Console - """) - else: - st.error("Failed to generate structured data. Please check your inputs and try again.") - - # Dialog for refining blog title - if refine_title_button: - with st.expander("Blog Title Refinement Tool", expanded=True): - st.subheader("Refine Your Blog Title") - - # Store the current title in session state for later reference - if 'current_title' not in st.session_state: - st.session_state.current_title = blog_title - - # Extract keywords from tags and content - keywords_from_tags = blog_tags if blog_tags else "" - blog_content_sample = blog_markdown_str[:3000] if blog_markdown_str else "" - - # Title generation form - with st.form(key="title_form"): - st.markdown("#### Provide information to generate new title suggestions") - title_keywords = st.text_input( - "Main Keywords:", - value=keywords_from_tags, - help="Enter main keywords separated by commas" - ) - - title_type = st.selectbox( - "Blog Type:", - options=['General', 'How-to Guides', 'Tutorials', 'Listicles', 'Newsworthy Posts', 'FAQs', 'Checklists/Cheat Sheets'], - index=0 - ) - - intent_type = st.selectbox( - "Search Intent:", - options=['Informational Intent', 'Commercial Intent', 'Transactional Intent', 'Navigational Intent'], - index=0 - ) - - language = st.selectbox( - "Language:", - options=["English", "Spanish", "French", "German", "Chinese", "Japanese", "Other"], - index=0 - ) - - if language == "Other": - language = st.text_input("Specify Language:", placeholder="e.g., Italian, Dutch") - - submit_title = st.form_submit_button("Generate Title Suggestions") - - if submit_title: - with st.spinner("Generating title suggestions..."): - # Use the imported generate_blog_titles function - title_suggestions = generate_blog_titles( - title_keywords, - blog_content_sample, - title_type, - intent_type, - language - ) - - if title_suggestions: - st.success("✅ Title suggestions generated!") - st.markdown("### Title Suggestions") - st.markdown(title_suggestions) - - # Allow selecting a title - st.markdown("#### Select or enter a new title") - new_title = st.text_input("New Blog Title", value=st.session_state.current_title) - - if st.button("Apply New Title"): - # Store the new title in the session state - st.session_state.blog_title = new_title - st.success(f"✅ Title updated to: {new_title}") - # Return to main page with updated title - st.experimental_rerun() - else: - st.error("Failed to generate title suggestions.") - - # Dialog for refining meta description - if refine_meta_button: - with st.expander("Meta Description Refinement Tool", expanded=True): - st.subheader("Refine Your Meta Description") - - # Store the current meta description in session state - if 'current_meta_desc' not in st.session_state: - st.session_state.current_meta_desc = blog_meta_desc - - # Extract keywords from tags and content - keywords_from_tags = blog_tags if blog_tags else "" - - # Meta description generation form - with st.form(key="meta_desc_form"): - st.markdown("#### Provide information to generate new meta description suggestions") - meta_keywords = st.text_input( - "Target Keywords:", - value=keywords_from_tags, - help="Enter target keywords separated by commas" - ) - - tone_options = ["General", "Informative", "Engaging", "Humorous", "Intriguing", "Playful"] - tone = st.selectbox( - "Desired Tone:", - options=tone_options, - index=0 - ) - - search_type = st.selectbox( - "Search Intent:", - options=['Informational Intent', 'Commercial Intent', 'Transactional Intent', 'Navigational Intent'], - index=0 - ) - - language_options = ["English", "Spanish", "French", "German", "Other"] - language_choice = st.selectbox( - "Preferred Language:", - options=language_options, - index=0 - ) - - if language_choice == "Other": - language = st.text_input("Specify Language:", placeholder="e.g., Italian, Chinese") - else: - language = language_choice - - submit_meta = st.form_submit_button("Generate Meta Description Suggestions") - - if submit_meta: - with st.spinner("Generating meta description suggestions..."): - # Use the imported generate_blog_metadesc function - meta_suggestions = generate_blog_metadesc( - meta_keywords, - tone, - search_type, - language - ) - - if meta_suggestions: - st.success("✅ Meta description suggestions generated!") - st.markdown("### Meta Description Suggestions") - st.markdown(meta_suggestions) - - # Allow selecting a meta description - st.markdown("#### Select or enter a new meta description") - new_meta_desc = st.text_area("New Meta Description", value=st.session_state.current_meta_desc) - - if st.button("Apply New Meta Description"): - # Store the new meta description in the session state - st.session_state.blog_meta_desc = new_meta_desc - st.success(f"✅ Meta description updated!") - # Return to main page with updated meta description - st.experimental_rerun() - else: - st.error("Failed to generate meta description suggestions.") - - # Final progress update - update_progress(5, 5, "Blog generation complete!") - - # Replace progress bar with success message - progress_placeholder.success("✅ Blog generation process completed successfully!") - - return blog_markdown_str - -# Local wrapper functions to handle the parameter mismatch -def do_google_serp_search(search_keywords, status_container=None, update_progress=None, **kwargs): - """ - Wrapper function to handle the parameter mismatch with the original function. - """ - try: - if status_container is None: - status_container = st.empty() - - if update_progress is None: - def update_progress(message, progress=None, level="info"): - if level == "error": - status_container.error(message) - elif level == "warning": - status_container.warning(message) - else: - status_container.info(message) - - # Create a fixed update_progress function that handles any progress type - def safe_update_progress(message, progress=None, level="info"): - try: - # Handle progress value of different types - if progress is not None: - if isinstance(progress, str): - # Try to convert string to float if it represents a number - try: - progress = float(progress) - except ValueError: - # If conversion fails, just log the message without updating progress - progress = None - - # Call the original update_progress with sanitized values - update_progress(message, progress, level) - except Exception as err: - # If there's an error in the progress function, just log to console - logger.error(f"Error in progress update: {err}") - # Try one more time with just the message - try: - update_progress(message, None, level) - except: - pass - - # Set default search parameters - fix the parameter to use 'max_results' not 'num_results' - search_params = { - "max_results": kwargs.get("max_results", 10), - "include_domains": kwargs.get("include_domains", []), - "search_depth": kwargs.get("search_depth", "basic") - } - - # Update status to indicate we're checking API keys - status_container.info("🔑 Checking required API keys...") - - # Call the original function with the required parameters - result = gpt_do_google_serp_search(search_keywords, status_container, safe_update_progress, **search_params) - return result - - except Exception as e: - error_msg = str(e) - logger.error(f"Error in do_google_serp_search wrapper: {error_msg}") - - # Check for common error patterns and display user-friendly messages - if "SERPER_API_KEY is missing" in error_msg: - status_container.error("🔑 Google search API key (SERPER_API_KEY) is missing. Please check your environment settings.") - st.error("Google SERP search failed: API key is missing. Using alternative methods.") - elif "Progress Value has invalid type" in error_msg: - # This is an internal error, log it but show a more user-friendly message - status_container.warning("⚠️ Internal progress tracking error. Continuing with search.") - else: - # For unknown errors, show the full error message - status_container.error(f"🚫 Google search error: {error_msg}") - st.error(f"Google SERP search failed: {error_msg}") - - # Return a minimal result structure to prevent downstream errors - return { - 'results': {}, - 'titles': [], - 'summary': f"Error occurred during search: {error_msg}", - 'stats': { - 'organic_count': 0, - 'questions_count': 0, - 'related_count': 0 - } - } - -def do_tavily_ai_search(keywords, max_results=10, search_depth="basic", include_domains=None, time_range="year"): - """ - Wrapper function for Tavily search to handle parameter differences. - - Args: - keywords (str): Keywords to search for - max_results (int): Maximum number of search results to return - search_depth (str): "basic" or "advanced" search depth - include_domains (list): List of domains to prioritize in search - time_range (str): Time range for results ("day", "week", "month", "year", "all") - """ - status_container = st.empty() - - if include_domains is None: - include_domains = [] - - try: - # Show status message - status_container.info(f"🔍 Preparing Tavily AI search with {search_depth} depth...") - - # FIXED: Ensure all parameters have correct types to prevent comparison errors - tavily_params = { - 'max_results': int(max_results), # Explicitly convert to int - 'search_depth': str(search_depth), # Ensure this is a string - 'include_domains': include_domains, - 'time_range': str(time_range) - } - - # Log the parameters for debugging - logger.info(f"Tavily search parameters: {tavily_params}") - - # Check for API key before making the request - tavily_api_key = os.environ.get("TAVILY_API_KEY") - if not tavily_api_key: - status_container.error("🔑 Tavily API key (TAVILY_API_KEY) is missing. Please check your environment settings.") - st.error("Tavily search failed: API key is missing. Using alternative methods.") - return None, [], "API key missing" - - status_container.info(f"🔍 Searching with Tavily AI using {search_depth} depth for: {keywords}") - - # Direct implementation without calling gpt_do_tavily_ai_search to avoid type issues - try: - from ..ai_web_researcher.tavily_ai_search import do_tavily_ai_search as tavily_direct_search - # Call the function directly with correct parameter types - tavily_raw_results = tavily_direct_search( - keywords, - max_results=tavily_params['max_results'], - search_depth=tavily_params['search_depth'], - include_domains=tavily_params['include_domains'], - time_range=tavily_params['time_range'] - ) - - # Extract the needed information - if isinstance(tavily_raw_results, tuple) and len(tavily_raw_results) == 3: - # If already in the right format, use it directly - return tavily_raw_results - - # Process the results to extract titles and answer - t_results = tavily_raw_results - t_titles = [] - t_answer = "" - - # Extract titles from results if available - if isinstance(t_results, dict): - if 'results' in t_results and isinstance(t_results['results'], list): - t_titles = [r.get('title', '') for r in t_results['results']] - status_container.success(f"✅ Found {len(t_titles)} relevant articles") - if 'answer' in t_results: - t_answer = t_results['answer'] - status_container.success("✅ Generated a summary answer") - - return t_results, t_titles, t_answer - - except ImportError: - # Fall back to the original function if direct import fails - status_container.warning("⚠️ Using fallback Tavily search method...") - logger.warning("Using fallback Tavily search method") - - # FIXED: Alternative approach - wrap the call in try/except to handle type errors - try: - tavily_result = gpt_do_tavily_ai_search(keywords, **tavily_params) - - # Format the result to match what the blog writer expects - if isinstance(tavily_result, tuple) and len(tavily_result) == 3: - status_container.success("✅ Tavily search completed successfully") - return tavily_result - - # If not a tuple with expected values, try to extract what we need - t_results = tavily_result - - # Extract titles and answer if available - t_titles = [] - t_answer = "" - - if isinstance(t_results, dict): - if 'results' in t_results and isinstance(t_results['results'], list): - t_titles = [r.get('title', '') for r in t_results['results']] - status_container.success(f"✅ Found {len(t_titles)} relevant articles") - if 'answer' in t_results: - t_answer = t_results['answer'] - status_container.success("✅ Generated a summary answer") - - return t_results, t_titles, t_answer - - except TypeError as type_err: - # Handle the specific type error more gracefully - error_msg = str(type_err) - logger.error(f"Type error in Tavily search: {error_msg}") - - if "'>' not supported" in error_msg: - status_container.error("🚫 Tavily search parameter type error. Trying alternative approach...") - - # Try a simpler approach with minimal parameters - try: - # Call with only the keyword and fixed max_results - tavily_result = gpt_do_tavily_ai_search(keywords, max_results=10) - - # Minimal processing to extract titles and answer - t_results = tavily_result - t_titles = [] - t_answer = "" - - if isinstance(t_results, dict): - if 'results' in t_results and isinstance(t_results['results'], list): - t_titles = [r.get('title', '') for r in t_results['results']] - if 'answer' in t_results: - t_answer = t_results['answer'] - - return t_results, t_titles, t_answer - except Exception as inner_err: - logger.error(f"Alternative Tavily approach also failed: {inner_err}") - raise - else: - # Re-raise other type errors - raise - - except Exception as e: - error_msg = str(e) - logger.error(f"Error in do_tavily_ai_search wrapper: {error_msg}") - - # Display user-friendly error message - status_container.error(f"🚫 Tavily search error: {error_msg}") - st.error(f"Tavily AI search failed: {error_msg}") - - # Return empty results to prevent downstream errors - return None, [], f"Error: {error_msg}" - - finally: - # Clear the status container after a delay - time.sleep(2) - status_container.empty() diff --git a/lib/ai_writers/long_form_ai_writer.py b/lib/ai_writers/long_form_ai_writer.py index ab6db880..adea4d05 100644 --- a/lib/ai_writers/long_form_ai_writer.py +++ b/lib/ai_writers/long_form_ai_writer.py @@ -175,7 +175,7 @@ def research_topic(keywords, search_params=None): placeholder.info("Researching topic... Please wait.") try: - from .keywords_to_blog_streamlit import do_tavily_ai_search + from .ai_blog_writer.keywords_to_blog_streamlit import do_tavily_ai_search # Use provided search params or defaults if search_params is None: diff --git a/lib/ai_writers/speech_to_blog/__pycache__/main_audio_to_blog.cpython-312.pyc b/lib/ai_writers/speech_to_blog/__pycache__/main_audio_to_blog.cpython-312.pyc deleted file mode 100644 index 2fbd1937a94fbbbe9e701373ec62745fbbf176f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3816 zcmb_fU2GHC6~5!~jK||7PDo4~0%5?RZ3Ho;3tMa@bRj>0V4)N$gf}bi;JI-o%y`^8 zH-rQmFA~y1Rf)Xy0U`B~Dt*W!k8HaOEA?gAq#_!ETB+MgyhVkrDtPI+GoE;xsGzFM z!`w6HeD|Dl&pr2?JAZ6$mJvMU@4Lqrf%+@^2p*wMy!a;&zeXC;I0NPUk;`%3cPJNv zF=X&&I2Y!8nUE8J#~Y$4=0r1+i`4m2P6B?|h?;Uv=1>S>d2(}LtTE2l$vPt0kOUMC zO>PP3#;6IJ_`AqF?{mPWq(zMuGoFhBEo(8O)ojbPd32Mp$!yQHdvvqWVJ313k8UwG zo1M8%pyM}CE;+;XwLWL+PW5rFs9_D4XkW+;(_d(&D^jACikL|Zt60S3-0blI17F6* zp{w3d9oXxCRtAa$E~?4z)xsazGzuR1Lti_%J5Q2VFCo37%D%l z(}_I6Yusj!YhtQuDpdj2W~-O6r;%x9z809p;AsYeS$Z)V_OgMia#2Lr5R;(**ci1? z1>He+L-TbMhiH&fsDc9S7|g7Os{C8EiNOre6A!G^_<&m#Xj{Oo2mzNvZ`CU`u(UnU zP!npQ`TCl?l_mm7g$wXw5Q(Z-jZ~#-6s&YM5H+zDsY$gcUCZ8i(U*en-2qN(s)=`y z)?ACiyQL<25Uep&EPh9#mp?P=#B!3vb@FcD8F;$KAEa za%e_KAzEJnLcNF_tcJg1S88|~P#Rweud^BpWRpz+)flyKup$m!K$=huUklH1zl!

8&%5T{hxg!7PzIf4=ugMVY0%!;7zefA* z+GB6fUR|%fFEM-aoO%h{imDj8O%-cgsTVipvaVqZ1SL%=;u0n*1sE$?3RFV{tAz>0 zDq%K*PGB=FEAY5zcKcbX5~`Pq3bmAJqEqZovP#O&e|9pf%x)PW1Qh6{z*0~~vkHkq zI-Z){v!;TTOt8Z0wGvcL&pfD7s7?)>p53;l56Z8PU8}NS?)Xm$E~|t&r23u|tP+Jj zqKp&ERMaU=w-mcT^fL9*8lioG(f zmsG~w+f{__h3%~J(QMbp8vs--dZONq zq7R`ZDH`^J0{t?gBIqLJo`iNHnr|iM)i_zPe43a60qQ8~4b@{%5C+ykdh?| zHw}u3&6XDIJsV%08On~GKYnWX^!c-65CMwK_kJ{X_;~+$cF4$>pp6qozQ(hMXM6(>UP43)sP@`tVk`l zW4B_?<^v0t7Cv@5hL)t^m94w)omkv@@Uhf&GcuQ4mBj|GoG^_T9tX>Ft>8}lymx%#lz}j>CjDi?$mFN z->a=kVPTm2M(T35?7o+~-}6#K9#g^NwaT zc{Vr}QV6-{7F&D5N&MuA^ugEC2d`IoCVl-fKn^#M1NYC~A8`^#o=8JqOG6%Vn0sYk zh6K6sy`c=6-#t8tKJVNy%AwCQtw#m)d%5H2e)NY-bU4d@5$Yd4z<+T-VDw;g*l}e z7J%;SazmEwN^oh$R5vkUx#~(3U!l`PE&Hciw92@o>ICkGh_EX!{4)U7`%{=AQ+|;V z2{x_E*yUACb48nKaIJT@dk4rn?{=QGhh z!{38f2fgF`Tl4~AfXENv^-3BrEp5uche#Ga*-g~G058Y^j(dutPf`3SlAodOr>ObQ zXxmd1`y1+7M$Mm!pEX}^z7bzWGLZ6h`R04Kv$wL#ZHeW0?puN9#BXK94L(CBpP|ub QXaL?X#J!xskql`6H*ZO{g8%>k diff --git a/lib/ai_writers/speech_to_blog/__pycache__/write_blogs_from_youtube_videos.cpython-312.pyc b/lib/ai_writers/speech_to_blog/__pycache__/write_blogs_from_youtube_videos.cpython-312.pyc deleted file mode 100644 index c883ceed2f70c8f769ede15b953cd0c42abd7c80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4550 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 diff --git a/lib/ai_writers/speech_to_blog/main_audio_to_blog.py b/lib/ai_writers/speech_to_blog/main_audio_to_blog.py index 432d4a7e..677bb19f 100644 --- a/lib/ai_writers/speech_to_blog/main_audio_to_blog.py +++ b/lib/ai_writers/speech_to_blog/main_audio_to_blog.py @@ -17,7 +17,7 @@ logger.add(sys.stdout, ) from ...ai_web_researcher.gpt_online_researcher import do_google_serp_search -from ..blog_from_google_serp import blog_with_research +from ..ai_blog_writer.blog_from_google_serp import blog_with_research from ...blog_metadata.get_blog_metadata import blog_metadata from ...blog_postprocessing.save_blog_to_file import save_blog_to_file from ...gpt_providers.audio_to_text_generation.stt_audio_blog import speech_to_text @@ -110,13 +110,24 @@ def generate_audio_blog(audio_input): logger.error(f"Error in blog_with_research: {e}") sys.exit(1) - try: - blog_title, blog_meta_desc, blog_tags, blog_categories = blog_metadata(blog_markdown_str) + try: + import asyncio + # blog_metadata now returns 6 values: title, desc, tags, categories, hashtags, slug + blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = asyncio.run(blog_metadata(blog_markdown_str)) except Exception as err: logger.error(f"Failed to generate blog metadata: {err}") + # Set defaults in case of failure + blog_title = "Blog Article" + blog_meta_desc = "An informative blog post" + blog_tags = "content, blog" + blog_categories = "General, Information" + blog_hashtags = "#content #blog" + blog_slug = "blog-article" try: # TBD: Save the blog content as a .md file. Markdown or HTML ? + # Initialize generated_image_filepath to None since it's not generated in this function + generated_image_filepath = None save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath) except Exception as err: logger.error(f"Failed to save final blog in a file: {err}") diff --git a/lib/blog_metadata/__pycache__/get_blog_category.cpython-312.pyc b/lib/blog_metadata/__pycache__/get_blog_category.cpython-312.pyc deleted file mode 100644 index 5c1d917b6bdc81eefcc0081df5d6b720e5daf123..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1611 zcmZux&1)M+6ra&**DFQ-NF9@Y96BTcqc&ArhhBtnD;m3)xHeP<6Cp5b?P$GbcSp>O zEUzpT8v=1ofnItDhFn|sQ0QOKTc8)07HT%Rg%)xXbpyq{^v$jm*RBV4=FPmfzxUqn zy_uhehqDL<|Kr{Id>WzWYBLymUpW2>gs+i}Z0w;*vf>JceacJuT1CSsg{Zb+4t_jc z5Bg%?BKCEvRT4!Txw9u~XKQx)JN-aQG#J^2J>(7fX2k^AyoV~85a%;5lzK6bBg12k zL-}%he%hm()SKCkm+tiY$+k1?a7u%j?YisHZgJal1GUtfK~1_W0J}urA=^F`!g6Rg zpNh;H^B8yAG&1Up`<8r>0)OBx=Xw6Bq+4>;p+OBNLB_M9Oa*=XJxo9u?x06N)4tZl z@|E3&f;l&A?Vzv6D1+pHdep&#^KPn>Dx>R28iQYNAbzc@{Rj7g0@&$}_6Y5#1}GTa zMYi76zSKfJSzEY;wzRvbh4V&xE=yp{^?4zZ5y+5oOH#519FrPI4sb#R0ZNGDZqlHy zVS%K9EW`j-0LhdDu>wN38k9>?o?U{SO=^i5X>nIlev{0Tmg{+>!P%y3QxdW!9LV(x zFjZh`l$%g$Aqfk-kWBqAtk$|)TPGH$#Ajf|DRh7pNR>+8iG2qAW*l$=O~_hEAdEn| z_0SC*BG&=Nas&9g;noVIGB9 z`C)-9sT??Mz#L~?W4><@K^vB$+$Nir*QBCA&cH20P)RZRT)Lu2Cfh@k*J87F;j^S< zDuIiNrjj}>T%r9DB(SOsS5K>3oU`Dj*hvE@7DWTHrlii9PZkzFFOtQ)&Q+&JX*YmY z7*b+pcB@7k>PAI6<(%<+I?Bb!7ME_#gNp<;@EbBR2Z)gdh5}w5q)Kpd`_p1|Wq$GY z(n`51LLsStYpPnBe{Usjm#XKkRbxD#O3SvSRXrbjSZIXN`1w=lSAH3MjKC)xn08Rl zOuu%$XQCS)JTCMQ{!s5_^bvDs?ynruFWtYhd*^WMlV8)NUMj6G<7eYcW%qli9CqYf7HrjvBR+-+!o~P0%05Mz-_aXK yXn4oCKfF79Z}bRdLCo%E_ijBbJ}4eto;Vtwj?yo~vqfblPr?Y<5G diff --git a/lib/blog_metadata/__pycache__/get_blog_meta_desc.cpython-312.pyc b/lib/blog_metadata/__pycache__/get_blog_meta_desc.cpython-312.pyc deleted file mode 100644 index 4fac3bda19d43d66cb07c261ae12522a7fd6947e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1575 zcmZuxO=ufO6rSCc)+MJ zuPb&fVyq*rl7Osa{(GW!j^QMJNbVUOjYb^P8Sn=Jt6-sQ-6e(8x|U15ROP3jX9~yfi01=xiGkt^i<@n-$N8W`5K6m|7||R+b)TXxGs<=w<+6XuHtN z>5*0DJ{R5wL}rDHz?Ls{#6!4@?*}CbYqI15D{Gw~<5^Lrf3H$*JqD0$rH7O?g#%0W#vyC8S*0 zl!aH7oW-{{KD-VLge^Vh($AE^Es!?7*0n;WDH2KLGQ=KP>}1@oAPZ9=wHbxADhL@P z*{q9E7oG%hflgyoJOpafGD_fM4P6>2|Gg+)?;p*X1ST)geulQQ3_@MU)KpeW5K3AV zHa0^U!f|>7ph`W7=dzA%Loa`H>Eh*&FVXU{t&BjRP2U;Pwdy%4F4y;hDn`gFaf)5s zv5>CXehm~DhJgrFfbA$JeIH*!Sml^--IL3dGk+bY*v~qziB5P>LFy!umRY=g}CHbdq)J04tIgy{|ec{+Bejcw3b8-IdMSwUhf3L2xXVA<)-o44}wxuj4% z93?!4m;*{o&8(FHYtLDf1R=P{C8CUKfl@ci$gIX~6Infnk%5LGz9~fY)#1!^zPLCy zf9>|-LNQzqB?MQm6sP7cF2?08`rcA8_NOZ9*wQY#AWQv|lB)4|bv+uzKTdTRbacl` zFGdh*5WldD(-!&5BFPi?ZfxJ!KRNp>alNf4M&4*!Wc0mn?>zpZok|Rz-?|o>Cl=8xKs0CQoM~@^|XYO zIJFH;)1H&`bCUXv|4$tiN1{)Fuhg(YH%Ddbjgd&!}6W>&j$N+ApT=9~B4ym{|^ zZ+_0@48T9~$Hms08UVkEP4Z>N;B?5r1AqX6E~o>fAk~GQQdgkBnyY$RU6VNDW<0&F zORT$7Ubdd)7@#S4+B50~1PZ{$_Cj*OWZfUbvBzr@OM)=iem%jH6=l)%kH)?lYxrHW zXyzWQ&vl`ddnS~%XOK$^vh!{1V`5Xhx#b4!O@zZHaqdtj@VPP9#x&MbyB%`HE2*N% zkF*2g;IaB_5b97Db8nqTY-4h(S1r5vE_PRT{nC!>_+n{w z{H92N`^EOi-0^T2+HKso6qaoUF2A^kncfPBXVd1mOj)cTN9<4k;oxJvC`yat=*Fw&`JNBb(Z0@4~Iyc%n1U zOo8cyC6hs;_jW}b5p!Vjw(}q{R%ZJz|&D)!w)9R8tL>Wox60W z%W^OH5b=r&{13n4VgP=hfA4W)c=gHp!Te`~Pmhb2_M(Ggc~CpnjQi`m>!Wj*hZjbd zuRT#lh1ElC?YQvP_wGUA;-H#3EDx*0k4J^8huXE{1#9nvgM|+VH&W~3UNBl*`DtrZ zxPGWrew`_P_vPd6aA{;zMq1^RrvNOaRk>Lm@5Qu)j~q!Y8M6YK?dF;i`Xm>yCTbg1mf4}iSNZz4r5o@dKmfI2Q-A@A_5 zczt1oi$7He;V-~=3O+gk@1B4wCt&F*IDZ6k1O0w(H+L_81Pl(1UE}NJZ%f~lj^>L; S`TE}(73zP@8gLcLJN^X#_Jt$> diff --git a/lib/blog_metadata/__pycache__/get_blog_title.cpython-312.pyc b/lib/blog_metadata/__pycache__/get_blog_title.cpython-312.pyc deleted file mode 100644 index 641479772b8679d157ccc221b28bdc27c19d367c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3000 zcmd5;OKcNI7@pm=*Y?JCUJ%;y$RN_DF2PhFwUrg63MHTvFjb*Kq;8|dJ8>5FVa<-? z#IY-=kO(J0szfRji7TRq9=Ya*)Jvpta2F0pRV#6GOQ|Y)=|8hJu~S;LoEXWo|2+QZ z_jzW%OC&S|jsCoQrW>Rm`JmOpP2urZAY4ZVG9(L41z%}Og0XDLwlbwiNJd0C8*M4B ze{VO%<__U&eU(c@Ivekt;UhyaLf?dME5V9{4AqEO5j#2+1vzF!FG^Ff1u3OH;v2Fl z$yY76P$2YVWpdOabHo~36rbAY-e6cAEl@(7vBeqFBDKt-Wjg#bQ*!dmbRF0itXJ`( zO*~I8kXlOiqj}eIsaYYuI^$AXXOCp?2kzpQWlu9Q&!!8+$%le4MIVEnhy6G%iK0oh z5CbV_XSw=@X2`dj>k_I;UUyaY5{6P$ZXhEh`fwG2tgb|AA!n#6(Y~rodyI%!i;6y0 zm0*Wfm7(uiiNU@eu#z<5Vl^T9q}bE75}AjHYM~fnEqGBi)QX@Mu7*#bU5KfzAcqhg ztf_4oJO^Z>yQ3*%&m`MtOZ=T*2ID* z>A1X>mhR~M&*7yzQpl15OwSQ~ge(Z#8T^SeXA(fIFoH`Ck8gGa7u_ln07&ip~8;l5^0@0Ee{o z4VB#g=8{gCncv9f=kuoW?LU&{Wrg+nO9ktAP-_m_&O|VQ3xr`Rjy5=LW~gi9 zqeqWra5fdD3fyRZ$aLUZiEuGC0cRwf?5^X738gMgMf^Dbe@-`H^rNk_!A~~-hSR~p z;A>6^ZhyfVUxi!7E;2vb^6M*L&f`ABwSw`}@eeY&Q z_$5PgmU$*89NUEHhR*aHq%kdKgpYJ_!S8R~kZtpHGx$it&-)#^^QaL;$)59v9)z?@ zM=l&$>v{8DDD^{E|JT`u92yveRs7bn)j;y@frkV5YHn@kvAfpY&(}WvtdUUrbm_AC zOA={)*PMnd4eYA#+H-THfkJ!R6}La!{`&HV4I~ZksP7nA+r9_Hbga=E>x+Z-CY0O; zx`QoU1$5PCPPaYWHnf}pg*Rf2L}Y8cfg-W^l6rNZ(Gw2$J?J02sea!-ycDf>Z&~X8 z<3ZP^Uy%&+(d3o*<@nl`_wMxH39oG)yBB@Gp6p$U{n-e^@>A~>NPRUKAK!{rx5f@A z%IeT-<73L|SQzA#lFdHiZ>yB%%dY1uP<-)FEYVWH2bB`rog==Y8-}lX%z(#-zpqea z)6KU7_0p|Z9)F+!Thcb1&(EHI%}GHifyXWM7#t$H3A(4zQQI|278#=$hCC8)47!FQ zNz#23yN|S=(5rQnIImtxTu5B(t|JXZ?Si(n|4Qa^rrx``-aYjstVrq;4M|z)H+5L* Iqk~}LFVy>J@c;k- diff --git a/lib/blog_metadata/__pycache__/get_tags.cpython-312.pyc b/lib/blog_metadata/__pycache__/get_tags.cpython-312.pyc deleted file mode 100644 index 74f0493fcbc8df53503cd5d30fd9b2724d24d832..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1464 zcmaJ>OK2NM7@pbHu2!;SH+Ed(9_lG=C?bst&ZVfXQnj&z^RPo4Y9a(;qa7)0_SKn@ zY|B!xOCZiEv=;|LAlKp^dTTB{bt;$2T+k zOFErIu-Ko+tBDvwze}ONv>vnf9WYzSLKb#VvAg3U23c_wS1qa-DTt~|MnCXy-|n$J zkEqt6RdN(9rC!%a$x^M@58AfcwP0lFR@{lZM$rJ?xPgj^Rh&sYljgY$4t2+`)hN5% zoSkxLgF5-OXlqY>+!b3>HAbnIU#r>5s`K66d+Oo4h(F6B6GSH>Nni36xVKy0ENdnNC` z!L7AochS9rEEu(7-(X8`$F9eg@iJObub^d|i8l*L(pxh)=NlOYw~6Vww?Y?wN8@pAd%rTMRBPfuTJ{#4+^^ax!EC=+C1dLAUJ8@0@A zLD@$K*?rGhC6k0vI8)OTks}Xi#je_wZiK9V(BS_xk|pyp7o!2tWm{Z>4qewIoCYQ{ z1+_@Sbn296$zirHUKu1u$Ue?CPn@A11%KPCk;wdC^9Mc`+2(sQCfq(t0#>5}s`J42 zI3-o)yX5@&3pp~EiHC~M!#MRCHuF6uuTmJZJ-{?B*~Iipg$DA}|+82p|6%$U1tFM5AL{XTLpr z=lY|g+4T#%iZVR@+xW3NgZIr{O&L7CKJ!9H+Q7|o8|QZ3I`ugA`L3dA!%s&h?(4sf ze7I?JhQ>CB{(3ra^f^*MZw%f}-Ae6@eez)BfwnW6e{7uU3?A7`{0+&p;e9>|Rewx< zhJH>Ij;RmzcM3#(NHpLx$xwya*Z??0zlu7mccIl8b^QhnRnxLUoeRsai*WKa2($fn z9-XxBWnI(u4jz9`8J$iv|MzGQsvstt**B(K->N$_&n7^WFPGBfWH-e5t8ef0;yh4)5thc$mElDgFVb&xKn6 diff --git a/lib/blog_metadata/get_blog_metadata.py b/lib/blog_metadata/get_blog_metadata.py index b74c1b73..bd164c44 100644 --- a/lib/blog_metadata/get_blog_metadata.py +++ b/lib/blog_metadata/get_blog_metadata.py @@ -71,7 +71,7 @@ async def blog_metadata(blog_article): progress_bar.progress(6 / total_steps) # Present the result in a table format - status_container.success("✅ Metadata generation complete") + status_container.success("✅ Blog SEO Metadata generation complete") #st.table({ # "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Social Hashtags", "URL Slug"], # "Value": [blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug] diff --git a/lib/blog_postprocessing/__pycache__/blog_proof_reader.cpython-312.pyc b/lib/blog_postprocessing/__pycache__/blog_proof_reader.cpython-312.pyc deleted file mode 100644 index 54ca758837b340516ed583bff0f5270594987e27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2690 zcmai0OK2NM7@pNivLwrP9M^VgFqu4EJB~=4wzk#M)UPDYn>dfgaSmPdG-mU`I7gPcSme5(+U=0$-~>VHidDK0OJhiztkkw}P@B zfmIDc*}$4fReX&Hgth*8jR)tn?qN=vxN@n=UKZfvTk^Yxo_1bf4W4VZ(UWT9QdOMy z&H1v;HN7Xz39|T|f2kTdB(&}`U$)&_t&MU>4trMDkQ@QD|F^1NV-%d~%YixZ8oKWDJOp$e$-#N?Q}IjH z9cP3YG%KD*Gr~HYt$@?v;bc2KsW!?RUWHu@?ZxAGM%6SWEgZg8}kWGHz_A<_jnL)e?usPqBoSu-Ui^O{5pPLd8%N!KNc$Bc|*q=*hVjN?(2 zk`&VlkQmA(uqQFAl*IusgH_2#n@aEKf6JA>vCk>g!WMN9Mm?Q=RHe!+)f<5Q2vw# z8$rh<-H>z81d#n)hU<1>2C-NwkJ+jbc^8OG0HUU*c|Dy|5`RgPB01*Pl9HSzLHeN< z)TutWky$)rQrU{(GZw)XMdI=_`(dwR(nTJ9h@=mj=Wi$K6e~&$MiR zPGT9mPD)yi;Ln<>Vb{qjH6)$bp+q7LmrEjHM}ZOCMvy~`P&+!}%uc#9KW|k#JT}X7 z1NpQAIjS1W78SxMe&P_~N1&a}VU*FWw`0jJgtv02p$th=>oM|z@NfFC}(5pIr zgC;m}LSwmt-RxRATK6iXTVSUUe68)!w3kpN7z`W|R@yc#Dp!?7^Q!rE@|JQ_xuxCI zzVEx+f2aTM{yY2c50v*E{YfsJ{-`vmly_#zZR$e(N^|GK=AH-5J>}-!h2Y~*<5%^U z>#rR7rfZ@8w@_at6xg_>5qfKA9_k-7fK&z%khh)L9K#(y3qc(ecKh|#z>{J zv2oX>0~PEK46d|nz7Z_9bS;E_Z)#s?`uEu`6m9(*2|Tb+Lutc@_gd}+N{t5|h7LXm z9ehy{`O1rDy;ZBh72qDd94)o)y}j*rYpJ#WVQAn%XuvTXd~WT5Y<)glH`0cd!_kpA zT5b!E^oz@z8%O%Y<-P!yHPI0La}z8U;67gbGsr3b*@|982&k4&D*e zih2Lj2z3s2%y~DiQ#xJd6zp5{6fh9Q@bf(MzHZ8SjeJ0N!W62AtTy<8ums@|3O_=T SN2ujbbnIzxvoJ_^z|wyXtLmQs diff --git a/lib/blog_postprocessing/__pycache__/humanize_blog.cpython-312.pyc b/lib/blog_postprocessing/__pycache__/humanize_blog.cpython-312.pyc deleted file mode 100644 index 2679bd2c8e0f563bd17786f37123f5d20216f348..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3020 zcmZuz&1)pb74M$b%#Oy=%H9tg+c7T7hCzu|0sCUaTEXjGV}X&wT4AxQAZog6W=h-L z)v2nU(acIg4hG97Lm(u<-jGWUVe<#<|KN)uIf$`O8xrtMcrlpGCBIkQJzALzO!uo- z@2h_A)qDNL{Cvy9Pygqq-RD64ix$@V%NR zg574Nc>2CqlBI<6b-GKG@OS6Va%dF=(ahiL_kv=__o7C0En-vmQ{=IIGn_&~A+5n~-O2 zVYx&Fi-NnT70Cg?PPQTu4+<`fArFElhzK0A zxEPI6o)q9{)hQ2WEFUqOr$sPGJ5GaIA)v{kArrWIM3YK+K$w>%ZCi(tiwT?*g+`b$ zUU3SU1R0J;$bZESl}?cwt5Q+1I$B1(>1xvrFBW-o$Bh*0$u7e*-Hx{%|qv;AWK2gyz;o<*o*ImR#|)8YEL)EFTR_JluLF) zYfM%r`3pDLQ7|;vSJ}qWjK*m9CbQEsYx**o)CK7*%}nO#dtj#obN6q7fr;irC|N4nb-+DFho992B`(*CRr(e5&@qYcw%WMB^{`~m- zQ%AG^J*i{+ks&8XbCev?&VRIU?6p9)K5QMm`swN?tH)1WK3>@R Wt{(V}?^>R};XiC#@-OITVB&uQ=Gy@P diff --git a/lib/blog_postprocessing/__pycache__/save_blog_to_file.cpython-312.pyc b/lib/blog_postprocessing/__pycache__/save_blog_to_file.cpython-312.pyc deleted file mode 100644 index c00fa45ac7d2ba7501369dae650b4a7abd682c71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5412 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 diff --git a/lib/gpt_providers/text_generation/__pycache__/ai_essay_writer.cpython-312.pyc b/lib/gpt_providers/text_generation/__pycache__/ai_essay_writer.cpython-312.pyc deleted file mode 100644 index a4f9c03d97233d0ee69c1311ea616d8918651b30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7280 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 m2vcU~b_dQRM3}_WiksKCK-G!v6*NnbaTv diff --git a/lib/gpt_providers/text_generation/__pycache__/ai_story_writer.cpython-312.pyc b/lib/gpt_providers/text_generation/__pycache__/ai_story_writer.cpython-312.pyc deleted file mode 100644 index 44764c0de1d9ccdb4fd83167e01dd1e3b293d2c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7452 zcmd5>TWs7`dM1bB+{C$PG?Hb@l9~7-%a$gJ8ebw`vP!JTN^IG%oES3Ps??C9p_fBW z4>^)&GKzzu4=Rwwx@dqHT^MkI0@cEP5cbKB*-O@P_GPA0A$K4nLAx!`w`uH$Mf}kI z{~)@bGt$NSJ|({Kw%;qMKp<4IkXg=QN&PhQ_Ci%*d?9 zT*bRjV{^W%J{H@Y#^qYBw$OHq#^?N3{j|+$fn4xvh_-nxoC{wKL)-sX%+*#o0RKXf z?GOGZZZX-<)>}4$6fnNI<(zWQbLhiH7m3=>B z?)dIG`?1V4BeyL2KJ*oN`@X;~FgLhqW`XVZOFb|xEW$Oe=n9c6#T3(q4l-I|h8Q`~ z$|_>ckQGf_P_3*;g%-1tE^DejCuR&HDufusOob_xixQD?ilq>9ggzr>d-*|U19e_G zkM#OS#LGZ?&fXS!oq3ia7EFawb{TQ!7<#HfG!M+>E%Bgf5r>k~1M8jn$`(l+1}`aA zf#^-#0f8-?D6+fE-i+{|Suk^qD1T8_%Flq-s0`P(N4Wsj2``RJa6)KN-F@Ix%>KsG#g?nmT)JE^p1i!g*B&?O(H$8`g}cxNDL+V_F6&&MeRsseG~4 z-rx^SJ3~!N60l(me&)OI_YqU?JxMZsx(lZwe1T_~i$`UaXvcuM*q8 z7JI)EeE$!dt;w$$AD`F^Mk?_`%dal)twdAn!B;k8ZO`jG418lIf!<#p=x2T&8Vz&5 zh>r5yFL@rC=?4B`LJ)q`Zze(OpE6d%XJod_{tUbuu`P3V_hY;PI%$FG$@zYrW zIxBc5u-M}ChwgBTyweVsIhfgchhOwNJwl@=;CZ8kcBhvdad&g?EMo=S(d)?bw##aD zJFW|E%e%`_m(Kl|+uA}g>dkdqa@?J{7z{C1o7-OwmRsDoa2;*8?OF^q>9DOF+8Pe>)%L*85#9 z2k!k_B^Iomt{i2-+XrH>uo&4Qap1dlDo5_`y652Xg?mMt_G(y_g?N86B6r@5mVl8` zgkcsVa&I|0&yu6tWc^MKZvJ2n%CJ-Uk9T4}K6*299rEV(OYW7$hD<%%M;XS=IE%3+ z*_v=#Y=`WF&P*?weZozaO_al1F$USEeKGuDxY%yrz3lTsIy?W5=yZP}`;~&N;9k_ei?ti)T z-(gJ0G-ZgANYer2TFhKd?o4MTRmW$@?;<7^$->0!rI8n8`8;GDLzlK>WT(rtpsvW# zk^&MYRuodf${NJW=5}g=eTdRb!0s?IbPb*3EdgGMl*USIK%t|JtenS^M5+h>Cmr4}`sLN1sc%kxe{dfV=ER;ZsY1chCc1F=aS+b&l zDpKM(1FD#!Q6Q$GWkiRW1%qfZ_V$6GvjtU?fmlj>PuCR5%!>0WWN&IDq$ew8ny7ZQ zfeh!tIF>C_8mcA}iQ)!Olu3~D`%E#X>awSyGs?W88F_n-C)K}x^ogcWIx2yh3oHy) zhJt25%qS9p8yY_i)s?2)0AXRDf}SyeZC!%WNz$~Ux5X7j0+AiztSmBOieN9wmQk=! zLjxeFCCY~02PLHfg(Or%P;`MXpzqlt;LM{ps%gf8z1oH+h;0@K)JL{5Majfb+jgKg z=p|}8E2=qZP9ens(NuI3svOXrC8?S%Ohci{ypgsMtx5V^0hXXzXQ6rrKmaOD#X{o_ zG(o`D1}IXuOq{H#8O39FLbr$^7t)HTXcXWka64;hMIa|xVp`J0Sz&4C7|;3gC*E<{{;M3}7*SK-?{;$1)iV{hSW>(FhE*lC0<=w~w<^T;;Iw!`U2V4TB=4KT#U_&AYm=Wg;AgNMg z*d7d!$Q1ws4m3dNDRF{A5CRu6NQ}ZTXeuGfl#xtpz-9($&PsAh9HZO}h&ouE zfT?mhiKwL}tCxbv8(yU$&-)u;71XF9mhdGCkdY@!4$+IRN|IP=bHWYy8h8_`ycG5T zRr<&yI1M<@qI*f=9E5>9&UJ}_9?;iEyxAW(LyKOQ!;vmJj!0QR@ez9sFWm+r>1BA! z^l1FAnE(QSS8z`i!Uu#I5DDHeiIlOx-jeOTxQYkFBnuX5#W>tE*w2Ho@uJUsW!@x8sMQQ&MF`Z zO_E#kl8Bm;AgZIQ0iYE~O~3;n3^J;@6@Cj@0}(<&s2d9@Tni5Gq!u%46d>BpVk*HP z24oZ+A#B66=7@W(-qP&)_S06O2#c z<$2lgC=8~CF0a?yz0n1E@Gol2$peWVj! zGiw1S36w-{ox_A8PSH3jV@eqj`#HLT;88{`?D<`3ueZ{JD+o_36lZa%)tMxrxID#n zE#R1?)aOxf90F@$$2JXHkB9`1VoQDJu~@Nr!wtbLN5oOxJ}niEbhhgt^B8FQ8I9HY_$yIKpz7J_F@H9r&@Imse; zKwJwDC9gpysPRy7tNH#mu#){!R%^AD1DnAP;YN*S*1}LWI`1~MsAE$+ScK!&TGTOw z`y8Wr6l8Nd+FjLPu6H^Ko@d#HrFiJqD5Ep zAHm*QqH(wh(%?(0LC6pcyoBoxfl@zWo+TJQToHP2e{efjIXu1^{;XV?dan|Gf4${O z-8bdqPt{x6c7>M0^@D70=y52%6sj}6#4BIDGP0bjoH)07b@g1GVW-(pJj|nH21X$P+s`n+kg{4@1fZglP=!O|bn`XS;vqz`|yPy3C zPLPtHoVmU8Z!fF|_raO!Ph)=_`v>1YhWW>0Rsm(MIQ3wR3D~d{bzvBwtxH`fNI))JRD64TYh^iqJH;SMg(El;n=mFS7};K_f9bt0L>$hvUiv5=_y z{jgKd&`QVS?%xZETc_8Bm+N7F z=#9t8mzTyiyHm@B$H~5pWU87>tt_ktD%Ye+GPRbRg~hvN_DeZim9w~dC$2uZE{OFo z7aDmiwB36DwpQ&pR22>_jn(~3tm}5U8a-V1`6D|v_YAKbdE9k)qieX@HT>Y%>UibP zl}gv}T2}_hidpu{tX9oxK(?*tt9Zxl&btHEovCU(b&IcuD5ZY_N=c@z^V7)RL~g6g ziM9B^Mtry$A6|>Uw#07+qf3RKME>yX3d_WI!f7|?0Ck&MHkXx3;I{yX^0C!@#T;h0}V_Kge7uU;MX!NaeQ z437^nzd3m1Y?ArBH+c3q^Z9`m=>Poi(8M0*x82zOt#}yz9}1!ILGEEkbo?Oqa4+^h zJji3uAcyUt(AhTb;p@?}0=F8#(N%$mp4B!ST}_709^qDdqGyM=)x$jY4Dr}=gpM8$ zP3-0#v27DOxJL;beYAtep4~Y5Xiw-JhI@1{I&p$~bQCE+I>AHF7YvSm!G+%0$9*A0 z-`T@`v4@9te}wGD;1S5f=RNp-Vh3Np{e6#y<63OaFrb)9VZMa-V8#A^)9&;-u?7yv zAX*E}Ph=MmrU95nXBvxs&OC)qh6b!}f+yiaPeD^oljG2bpEAsS@CPPf*{4y44L@PJ go-n;nn3tX~d!H~nzxK!3#M6C$>SIrlV=(c500L(k!vFvP diff --git a/lib/gpt_providers/text_generation/__pycache__/gemini_pro_text.cpython-312.pyc b/lib/gpt_providers/text_generation/__pycache__/gemini_pro_text.cpython-312.pyc deleted file mode 100644 index 623c4e862785dc3f1c4a30cce3f040da74d43ac4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2436 zcmah~U2GIp6uz_bv$KEQE!#q?h*Jv4N?Q^M29^ebZ6ytMAy94D6gOk%c9}9io4d26 zU1k-Gi8djjK0%@;nwW^84?Gf|P<$ZqWgBW(u9BFbCVeYVqrn%?-Pvh3{=~`dIp>~x z?sv~UKlgqYi-i$<v1m2!8l+9Og_tVo=z~u ztW=kIBWTK5nL`1DW%%=&a2+t%=kvOqXoSpgHe9z0Pi^!pn(6h%pkdL`Jm<-nn|kJ- zT20b|3-Wc|o2l;=GFr@dHf|=e39~iZT1Sk7Z>?Hbi!4O12RvKQB(#|CyBj|k!mMQ2`8+10<>7tPSn043KiLPtyMjWWTF?W!i*n@qL%(mK~fm-`55k}CiWx&ffF%YTGEk=GGi&JDP{D`=sQg^JYV zVPYM}m$a}eEkyj3SArqr2B(t#>^gUm7F~#~*~zX9X*vO?^6bB)3hzcFLp<9Y$cB!h2SF8 zXb~%u#5O?#m8mNQVk@I~#_3aN%aNgx;mmOE@bTf?(Va4+J)mFi9Jdtt)v7RVXcFS z>Hq*0Sx*%_c9yn6ljQ7@Qz$t(rw&mLdrtO(lh8R~)7G$&vs4q));zWt3^a4j^JtV^ z3q^-u)husYclVzM_uVc>`jiu_xpgbwmx@vmAXS}))(%Jmc9BW|0Cx@z4b4p80^5(U z&Z8nG#3oeGtx224k2JiQ#v{x}nrOI(nRBq-p<;pPzyt?koeEP4js$lIVbx98T}?R) z4hRwOj!t>NC+sq&V*S`u_R~kji8CeZY>{0U%G&qz#6Ws{Y&bJ`Z0zKCakdC?-t*G< z;o;sfqQj*%Zs>1~=L=2__MO%>Op4>oliYe3$GuoHgB+XcE6mcihOTbv?@xBa#?0m7 z7U;bTXhr7513${K%kjDR=YcOmpM|O~rtc~TzL)z~B_u}PKYH=#{En;7UX6Wv`bPRj z&&@NpO1JWNUay`Ut)6(jns{SL7+aB0M|Z7tW36>V?YS2og~U#&DzvY*pxB0c(XPd4 z*R}EL$eT;i+=Zc)Kt#-NKP5ZvB~y#Z)U_Qq+p5Xb-Q>X!S_z{>$K}dgr7CY;@Y2tqjS;f zradpB!n+S3$Nw&aR z%0n(`umF!DWRS22f+q8}oi}iwpSx)cWgBfCk347$&1zdaw2!w~@ z9-`0>DEt6*{DQh4pe^@NWNvW&`Mc4sCA9Sc>a3yI1?m0R#n^lC8VZ9Lz8L;s&*k)7 zy4KoWi)Vimc~1IW7P!D8#7GMY1Q(;-*SKqBDf-gl{-bve-`$^C;*LEEZ00iD6CMSQ LbDm5NfDeBG+9Yax diff --git a/lib/gpt_providers/text_generation/__pycache__/main_text_generation.cpython-312.pyc b/lib/gpt_providers/text_generation/__pycache__/main_text_generation.cpython-312.pyc deleted file mode 100644 index 04901241686dbf0592f20babccc03d3b2328ad9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4321 zcmb_fTWl2989uYunZ5abS)0qGfh>!SaUn_Dl}l@kaa_Q5frPSg*$$qw>#=8NR%d2? zS#~Qxkpk*N%o9ivk{?si2djPIkw71i`Z8uM*%^$Ks27Ph3y4$^sp@}bcD!qZa8mV1 zGv}Z4pY#3y`Oo<;^S5v~fS{3ob)~-vBlLIr;WTfBc>WBCn@B_=Bcr6we zg_)^VhpzUbPxSwZz2mk=Ko%zkPbtJmoH0FPf<6g-P*w#oC8|1BW(xkEo`XF=z_?FVGZ{?I7KZy|JcH#U zm#nVTchLSU_GJjh%8^THNyeqbC0SBvm(D8_x}+*F&*m5TOIfUGLI#)OE|Z;5WtB(; zZ2HnF$qM>YYDW+kQFB-kq}0Tupl5PAj5KF(R#K#tj_33g!CFpLH2jpB0GJ&FgVqG= zd7`8yR3$BCQW~}-PgG8ZZWM9w&?V0gx#)RwNZ;g&jB|G5euPR)$yIWfJSA_*SMuu> zOCwa=oCOwj6`3jL22u1%b53;zkr+q-G`mf)4pr?%wHa$v-EHaf-nWq+a%S#9WOs?H*(u=RS{SnIDYG{STTc zy~l#;CmAhaQ54wb@cT5K#1s5kdlAg#b(PO-5t;@J8{xEd?Q1|_abiPf8j9>R!Bl%2`sEs#pHjrKmozsqQ&zSA37A50m)lgoqIW%lT`&;q{o@fZ7- z@ZDeEbG={@*Sv6Yg?($oheFK@#nra>a$CII*0mh$+Hm>89qaY2H-k5V#*ULK^{3W5 z4=(xc2Je0Nu>WD9+?iZp*V=Y3)NQaR(t2yhYP4%P+EtDoSPmc9vUb;@rT3O%#?MBK z9iuDtW9yyqFWy<&d$;r6lyUl;aXx8`UnqB`954<=dj9>wkD7NrLoQo^(2bC>d*I%{ zz4wfr!z=Z_T5F9iaNlisf&JZ!`tLoHMWGhJ1cR`V&qCKjM%(*K+7dQe`c~MVud!i@ z=#9M8egpQq+B~C;Kgta-?)yLLINImF-{%GT0pkYl0}nUQ z?S2sM7>Kza#JrU5w#E-~$6DPFei}X2;Qnia7w9cXiw0pUY5f_9n>8tG3X<5Xt(wM4 zOc9Y^6w#Yf)SJ`E^iQB{+Eo+cUzfmKMFi=@U1Z?xx$Uh9^-KvB(HRu?3NvuatPnRa zi6^FcDQ$&AEr%zhv;<*eXzZ*L90?Cqz{DihtSL}?NJLe#Skd_zfk?td8PnORvD2d; z4Idjk-EF}NnkHowUQk3H&*fAC;SwTe?L1g5=dm4+D9^4Dcv`Gbb+Qs5D47ZxzyM#D zlsxVUSOT^N=~@r04b`vTPzB<&AZfTJNPk2X8l=%B!4IeTi>f~PCaYL`+s+!iK(MnH zJ2#l2S}PQ|0joAj@Ugp|68&hNta0prb7wMiYSLVgCUFeghclJ5!sbA)RkGr?eAY z)B9lGt<}PC%Hzzb_SNF9#3=i=LG)ONE!!Y&`WI-(XmwTJgwl}up}$(zU1fBdwyZW~ z{Q@~(b}zDY#g^iX0dM7mPRCn^AeB>R=^q>BAzj+(8NT0m&;K8;^4fxi7DGQslWjas zQ8(cv4fd7?x3>zAH|&sR=@C)~1~el>gQtc^hEt$Y>crrAQg028jt!3VZw?j?jZ}qv z9|_=?WDfup8tn?HcE53-?QDC2Pu`)@0@WB|xgcqRQOEuKl_ICs>jej=fEvx*_R-rJeiC0XmS{voaU$yOs})sLE0 zH(C{w6`(Bh{SjHpTcPYF+Xn?;UdBgAAIP9lLz{rthMQrS$B2830*_JC6V(0$?fMD@ zuOC~8{$X?l?f55(uA%T{->2a#;ZN$;PyoolmB6*bpCzs*)*4#Y>XP4j-Hh+s0AjkI bpdUX$L(hC~Gm&Q}nFgl)`TlmM$x`CK0&7*< diff --git a/lib/gpt_providers/text_generation/__pycache__/openai_text_gen.cpython-312.pyc b/lib/gpt_providers/text_generation/__pycache__/openai_text_gen.cpython-312.pyc deleted file mode 100644 index ea34ba75c1bda84a1feee64a308f8387858a5c22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3908 zcmb^!TWk~A^^Tw8XJVTWNE0#v3^62WlA_fnP*64wY`P>tK$UJ9P zIlHD@4ul;!Q>rDXIs+Coy!uIw`_6lgDwabgZ}?RjgfM@Pt74~opvR^7^MR>AJ~$Q3 zho(X%UjTSRl3ViJ_geA*6O^_%@B1vgdeXKXco`S|zaouZyLL)oYdJbLN?Lt13 zMT* zxXNAglp1b2uK8RL&BX>OkZZTjVb67(kROZ_cswqUwAf(Tu;?%O&jE%rv2)v?eaNsn z2g(VyTnpNd$o|DfW3RQ&#VyF5rBJEyXXqE(GSN6-?UovUW5WNMXp5VxH&ZMd>_qmiHP_d+O1!J zZ<2_G0wzMHs6x-vQ~|n{Fj2tj(C9&37@iT0Q)kW#X)Rw+Fx&6(n`}cQs}GtG6H*|s zZwQ^bK@x+)`57$8YN2Qd2A+pO6S7#vL^SX=IZRK_mH=q0STXs#1g*gIhzdv=GtnU6 zA{L4|mdwK$Ab#6ak6kVvcV-;F@k8oO>1Mkd=N)hWqUc7__f^}%qI$LTWNW#$1p+U>KA*Xxl zPSe}dZ!3rPoiembSk;A2S*_PRCeF+GVqQ>-`HP@U%YZE=2di$2mj@Ya_1*tJsl zj}}StEGD{aEYwTs?HRBSsQ&A@#<$L6JKXfM70`NdeYNB`i^0X}w)}v+sHTO*Csk9! zmTGZ|GC0;wtaA%G1Z`wqHU@>!j3BDQ(3w#I6QYqWNCH*G)ugRqAa%_&Q{=pCRLKM_ zoh}l+YUVRHloerj);JAMoEaG(8cl(~)O#cECphXA3v%ibUZ5_>HNvi0$OcTkpvQn) z`Fz4l-DaXvzINT~)NhBCaw>J3p`acx7>jut$q@WO5v%Ei6jYR`7z>0wbEtW#V^yN= zj8fERE=Q{cXBs!cj2jd#cbpVu1yUYTpk^ooZ9*V9e3sQz&Di-g#uCs&3EM=Fte{^9 z?_Cbpu%@d9g+wzvOGGt`sf#^zs9RSsE>Mrg4k}Ymm6|#@J&KmiVnW^KiKyR> zHH`uhRAVi=Rq`-1_B0D@b?qAgi_KP%h*D3r0}*y{)4;YmC+xx|?1m=n{-%yhfpRI@ znAQ{patBMP^h{B`q|>O4*$QqnQ&f}`!3AX@RZ9Y}Sdk$aC~vg^>NX|NkU6Dl%#_Ph z4pS%Crbj_B>yq`~%d^9S)03m)$%)Bx)4G}G-|C+p8ttAWGIW$_MZP!<-7E!`&B_u~ z?=_*>DzV{q)?ww2A z1|Pic|ImM<@1xMB6EK^egj@LH8*OEN*LqufnQvKd>wxjkk>*=%H`{(qK3@3cLb-3~ z@xgaiBg0Fc_1NxPM{gcoiyf-O4z0!dDzU!xX!Ck|{I>@xI|s^5gX_&5>){55i#4zB z5jGppj{XpBk@aBfT5w+_xNmu6HF#*fwd3yCow1dUN^93z zYk#G+f30<}(mJ>_wjSKG7K~Sd@#Xw#@W@6mawC4L`)2ntSBdUji*{C`oomtFO0@TZ z<6-hiaCFnh#BYYtp<_?l#+HX3eDJ%)pNi$SF|Y|nF!Ao>oyl_F@RN9QW$0JpAL7Z! zZ6nO=1~9wh>*sE?XPo<*#*=^l%7cQ@>nA=u@f{7}E6eHS@0Obmuku}6G@EWX+e`fi5b%$O_au*_M{aNOi0e_v#l}d4VfOlx1N@`I zJcEz;*?2tmuHX4dq0_MIa?43bWs|(DC%+=iWyMUH9CHBg8HDJoUYbBpGvX6 zR;d)}Wvo2(88Hpl6fqk$dqNWShBx#0H|Pa`h$LZn$seo!6FW{OV9EaH=sFCL#T@q( z`JSS{QxtxRLQhd_(}TDjSG{W}Rzb1b2k&;>=_-Fy`y(oR;cHoHUh}n7d@Z-%e(c+S z)$=!pi*tO5*bqhzFC^p&w?bBf{gr|DJ|B8KFuux7eCasB2``+;afUNNG7ftG2|?=V AivR!s diff --git a/lib/gpt_providers/text_to_image_generation/__pycache__/gen_dali3_images.cpython-312.pyc b/lib/gpt_providers/text_to_image_generation/__pycache__/gen_dali3_images.cpython-312.pyc deleted file mode 100644 index 4c5d148acdad29124e6a5279d8774dfe3402bb1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2530 zcma)8%Wo4$7@zgp>-8gqkc5ERsH22JEU;ThC8*VcoCgpU!XrosV`Oc-6KA9Ku9+PZ zJ94TD^#CUhJ#auh^i(SN12_Ubv{EmQh?;0hrK&}hatowYE4cKV^=@nmeXQ)+Z|0l% zeUJIgH~U>jM-;(Bf89Hi3M2H3==hJ~2^&{IxQTS6OC~BwSxFLdzzme+tnA9N8O#P@ zENF&GN>(X_v*D7ORY4ch6*E$bW}^}cAfmuug|YBm^-iFP4kKYbaz|?R?neD_J+8+- zmF{>hWMefs(Xl3a35hGG%EZb{2+6dIMM5VNG7nM0XpL`km|f0mGmOx@#uzD;8Sk6d z43?*wrQ4-Esg`YvSj^DOHDMJ{dK~Qv$rp)5sK$t%H%eNOtW||Fz~z!*L8DdIL_T03 zDA0IN*eQfy$bTSxNQVE$&j5Q9MG*6XLTJtl)}Jcgy5t8fA-0L*2fP@nhvxhZXj?WH zY3q2R9;l;wpf1;gcjY@C(xSp*{>-{km;AIYhU?)eG>BN-*UoM6rPg~FqLYhioBvx| zI$zR*b#-o=-)p^Z!KQ=eH7jnR&x36-_xfw*w);}+tt+jW7myxaR6ka0;pTf@Vmthq z;OW8t^i-{@U^~!4WdCk^la3O4)%HB9$`{bQl!$7VU|XXYo?{+$9PCoZ!4=1_ikQt3 zJeE0eA~}wSamm(+iKjJ(=-9TfhKt4(Vqr$A440`5JWWMiuna9aX;%`^MyZ%@s_~G+ zXd;czwN_weW1qIJHP?KqZ7~ozuN%}~J7!Q)U^cDc`B_4VyT%95o6h5T!vsGG7KcJ_ zVL8SEabfmhyUYyR(oAq>N{rewuLE*Yc#O5#9(b=Rnt>MR<%(t+toADI z8Bg*5pl9_qObZVg7JJFd$x3OOP~oSyKW%w?*~|UjCY&X#LanxafZV6?X^qW_Sj5J< z>1pEmYc#_lZA+$V4%DCVs=?BDVg_p#CY0LXMxjulKo!eNxVHh2QVd$a1@1Icw6Mox zmSEGEW|~Qo9QIbtz+Bm#UM?CDMnoHKgJ^|H2YP0V; zn8wG?ojZ-En8qp&UWn|&c-1&*gu~X-#a^M6IXZS!aGm$(` z4YmYFC8R}7s{CnpL=p4rXIs?Ays4CjFj#&A>Rq{=nE-Oq>6n8a7nJ>Y|= z|IYW&e1g_@^@fg1kJT+VV%KAho)4GR!;icAKHG6k`DtM2%j6fym4QP`1BX_(y}8=m zzuMKi+VjThmd^DU8hn2PDTC35`qpn-QO{6g=*az@kGem)rmlAOUF-bwNvvxn*1r_% ze})2~QP)8Hdc4s$dO!2c$pw;{BvnRuoXJ?QKn zP00@iGQp9(^25Dx=sY}_98JhyCqf{@M?_?^Nb$g|PE}Z%h|`_$^7sXZtwJjx*MA3>IZO8D<(Ni-+`|9Sa2qw zS(hbA`VsAf-w!DIM=&S_o+4;|k0UAhR6&98Qhe~XblX{u4=<%pe0%0m`qZ*?`dMJN SbX?kyQQ(a9)Rk!p(Eb9btD+A8 diff --git a/lib/gpt_providers/text_to_image_generation/__pycache__/gen_stabl_diff_img.cpython-312.pyc b/lib/gpt_providers/text_to_image_generation/__pycache__/gen_stabl_diff_img.cpython-312.pyc deleted file mode 100644 index 9eaf319ff3e9a91d0e1ceb9e068a27e970dc8251..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1884 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