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 a285b321..00000000 Binary files a/lib/ai_writers/__pycache__/ai_agents_crew_writer.cpython-312.pyc and /dev/null differ 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 8c8a1399..00000000 Binary files a/lib/ai_writers/__pycache__/ai_news_article_writer.cpython-312.pyc and /dev/null differ 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 4bf5e071..00000000 Binary files a/lib/ai_writers/__pycache__/blog_from_google_serp.cpython-312.pyc and /dev/null differ 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 9a2b4028..00000000 Binary files a/lib/ai_writers/__pycache__/combine_blog_and_keywords.cpython-312.pyc and /dev/null differ 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 02c74633..00000000 Binary files a/lib/ai_writers/__pycache__/combine_research_and_blog.cpython-312.pyc and /dev/null differ diff --git a/lib/ai_writers/__pycache__/keywords_to_blog.cpython-312.pyc b/lib/ai_writers/__pycache__/keywords_to_blog.cpython-312.pyc deleted file mode 100644 index 22340c5b..00000000 Binary files a/lib/ai_writers/__pycache__/keywords_to_blog.cpython-312.pyc and /dev/null differ 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 57f64dab..00000000 Binary files a/lib/ai_writers/__pycache__/long_form_ai_writer.cpython-312.pyc and /dev/null differ 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 2fbd1937..00000000 Binary files a/lib/ai_writers/speech_to_blog/__pycache__/main_audio_to_blog.cpython-312.pyc and /dev/null differ 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 c883ceed..00000000 Binary files a/lib/ai_writers/speech_to_blog/__pycache__/write_blogs_from_youtube_videos.cpython-312.pyc and /dev/null differ 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 5c1d917b..00000000 Binary files a/lib/blog_metadata/__pycache__/get_blog_category.cpython-312.pyc and /dev/null differ 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 4fac3bda..00000000 Binary files a/lib/blog_metadata/__pycache__/get_blog_meta_desc.cpython-312.pyc and /dev/null differ diff --git a/lib/blog_metadata/__pycache__/get_blog_metadata.cpython-312.pyc b/lib/blog_metadata/__pycache__/get_blog_metadata.cpython-312.pyc deleted file mode 100644 index 610db03c..00000000 Binary files a/lib/blog_metadata/__pycache__/get_blog_metadata.cpython-312.pyc and /dev/null differ 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 64147977..00000000 Binary files a/lib/blog_metadata/__pycache__/get_blog_title.cpython-312.pyc and /dev/null differ 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 74f0493f..00000000 Binary files a/lib/blog_metadata/__pycache__/get_tags.cpython-312.pyc and /dev/null differ 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 54ca7588..00000000 Binary files a/lib/blog_postprocessing/__pycache__/blog_proof_reader.cpython-312.pyc and /dev/null differ 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 2679bd2c..00000000 Binary files a/lib/blog_postprocessing/__pycache__/humanize_blog.cpython-312.pyc and /dev/null differ 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 c00fa45a..00000000 Binary files a/lib/blog_postprocessing/__pycache__/save_blog_to_file.cpython-312.pyc and /dev/null differ diff --git a/lib/gpt_providers/audio_to_text_generation/__pycache__/stt_audio_blog.cpython-312.pyc b/lib/gpt_providers/audio_to_text_generation/__pycache__/stt_audio_blog.cpython-312.pyc deleted file mode 100644 index 2884d954..00000000 Binary files a/lib/gpt_providers/audio_to_text_generation/__pycache__/stt_audio_blog.cpython-312.pyc and /dev/null differ 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 a4f9c03d..00000000 Binary files a/lib/gpt_providers/text_generation/__pycache__/ai_essay_writer.cpython-312.pyc and /dev/null differ 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 44764c0d..00000000 Binary files a/lib/gpt_providers/text_generation/__pycache__/ai_story_writer.cpython-312.pyc and /dev/null differ 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 623c4e86..00000000 Binary files a/lib/gpt_providers/text_generation/__pycache__/gemini_pro_text.cpython-312.pyc and /dev/null differ 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 04901241..00000000 Binary files a/lib/gpt_providers/text_generation/__pycache__/main_text_generation.cpython-312.pyc and /dev/null differ 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 ea34ba75..00000000 Binary files a/lib/gpt_providers/text_generation/__pycache__/openai_text_gen.cpython-312.pyc and /dev/null differ 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 4c5d148a..00000000 Binary files a/lib/gpt_providers/text_to_image_generation/__pycache__/gen_dali3_images.cpython-312.pyc and /dev/null differ 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 9eaf319f..00000000 Binary files a/lib/gpt_providers/text_to_image_generation/__pycache__/gen_stabl_diff_img.cpython-312.pyc and /dev/null differ diff --git a/lib/gpt_providers/text_to_image_generation/__pycache__/main_generate_image_from_prompt.cpython-312.pyc b/lib/gpt_providers/text_to_image_generation/__pycache__/main_generate_image_from_prompt.cpython-312.pyc deleted file mode 100644 index cdbcca0f..00000000 Binary files a/lib/gpt_providers/text_to_image_generation/__pycache__/main_generate_image_from_prompt.cpython-312.pyc and /dev/null differ diff --git a/lib/gpt_providers/text_to_image_generation/__pycache__/save_image.cpython-312.pyc b/lib/gpt_providers/text_to_image_generation/__pycache__/save_image.cpython-312.pyc deleted file mode 100644 index d74336ab..00000000 Binary files a/lib/gpt_providers/text_to_image_generation/__pycache__/save_image.cpython-312.pyc and /dev/null differ