import gradio as gr import os import requests import json from dotenv import load_dotenv import logging from typing import List, Dict, Tuple # Load environment variables load_dotenv() # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class Config: """Central configuration for the app""" OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") ARTICLES_FILE_PATH = "articles.txt" # Model configurations AI_MODELS = { "gpt-4.1": "OpenAI GPT-4.1", "o3": "OpenAI o3", "gemini-2.5-pro": "Google Gemini 2.5 Pro" } DEFAULT_MODEL = "gpt-4.1" class ArticleKnowledgeBase: """Handles loading and managing reference articles""" def __init__(self): self.articles = self._load_articles() def _load_articles(self) -> str: """Load articles from file""" try: with open(Config.ARTICLES_FILE_PATH, 'r', encoding='utf-8') as f: content = f.read().strip() logger.info(f"Loaded {len(content.split())} words from articles.txt") return content except FileNotFoundError: logger.warning(f"Articles file not found at {Config.ARTICLES_FILE_PATH}") return "No reference articles found. Please add articles to articles.txt" except Exception as e: logger.error(f"Error loading articles: {str(e)}") return "Error loading reference articles." def get_context(self) -> str: """Get the articles context for the AI""" return self.articles class SEOArticleGenerator: """Handles article generation using OpenAI and Gemini models""" def __init__(self): self.openai_api_key = Config.OPENAI_API_KEY self.gemini_api_key = Config.GEMINI_API_KEY self.knowledge_base = ArticleKnowledgeBase() def generate_article( self, context: str, keywords: str, model: str = "gpt-4.1", previous_article: str = None, edit_instructions: str = None ) -> Dict[str, any]: """Generate SEO-optimized article using OpenAI or Gemini models""" # Check API keys based on model if model.startswith("gemini-"): if not self.gemini_api_key: return { "success": False, "error": "Gemini API key not found. Please set GEMINI_API_KEY in .env file." } else: if not self.openai_api_key: return { "success": False, "error": "OpenAI API key not found. Please set OPENAI_API_KEY in .env file." } # Get reference articles reference_articles = self.knowledge_base.get_context() # Parse keywords keyword_list = [k.strip() for k in keywords.split(',') if k.strip()] # Build the system prompt system_prompt = f"""You are an expert SEO content writer. Create engaging, high-quality articles that naturally incorporate SEO keywords while providing genuine value to readers. {reference_articles} Use the reference articles above to guide your writing style and quality standards. Your goals: - Write naturally and engagingly - let your expertise shine - Incorporate the provided keywords organically (aim for 2-3 keywords minimum) - Create valuable, actionable content that readers will actually want to read - Use proper markdown formatting with clear headings and structure - Target 800-1500 words unless context suggests otherwise Trust your instincts as a writer - focus on creating content that both search engines and humans will love.""" # Build user prompt based on whether this is an edit or new article if previous_article and edit_instructions: user_prompt = f"""Please edit the following article based on these instructions: {edit_instructions} {previous_article} {context} {', '.join(keyword_list)} Please provide the edited version of the article, maintaining the SEO keywords and improving based on the editing instructions.""" else: user_prompt = f"""Write a comprehensive SEO-optimized article based on the following: {context} {', '.join(keyword_list)} Remember to: - Incorporate at least 2-3 of the provided keywords naturally - Create an engaging, well-structured article - Use markdown formatting - Aim for 800-1500 words - Make it valuable and informative for readers""" try: if model.startswith("gemini-"): # Use Gemini API response = requests.post( f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent", headers={ "Content-Type": "application/json", "x-goog-api-key": self.gemini_api_key }, json={ "contents": [{ "parts": [{ "text": f"{system_prompt}\n\n{user_prompt}" }] }] } ) if response.status_code == 200: result = response.json() article = result['candidates'][0]['content']['parts'][0]['text'].strip() else: return { "success": False, "error": f"Gemini API error: {response.status_code} - {response.text}" } else: # Use OpenAI API response = requests.post( "https://api.openai.com/v1/chat/completions", headers={ "Authorization": f"Bearer {self.openai_api_key}", "Content-Type": "application/json" }, json={ "model": model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], "temperature": 0.7, "max_tokens": 3000 } ) if response.status_code == 200: result = response.json() article = result['choices'][0]['message']['content'].strip() else: return { "success": False, "error": f"OpenAI API error: {response.status_code} - {response.text}" } # Count keyword occurrences keyword_counts = {} for keyword in keyword_list: keyword_counts[keyword] = article.lower().count(keyword.lower()) return { "success": True, "article": article, "keyword_counts": keyword_counts, "word_count": len(article.split()) } except Exception as e: logger.error(f"Error generating article: {str(e)}") return { "success": False, "error": f"Error generating article: {str(e)}" } class SEOArticleBot: """Main application class""" def __init__(self): self.generator = SEOArticleGenerator() def generate_article( self, context: str, keywords: str, model: str ) -> Tuple[str, str, str]: """Generate new article""" if not context.strip(): return "", "Please provide context for the article", "" if not keywords.strip(): return "", "Please provide at least one SEO keyword", "" result = self.generator.generate_article(context, keywords, model) if result["success"]: # Create status message with keyword counts keyword_info = "\n".join([f"- {k}: {v} occurrences" for k, v in result["keyword_counts"].items()]) status = f"✅ Article generated successfully!\n\nWord count: {result['word_count']}\n\nKeyword usage:\n{keyword_info}" # Create a preview/info section info = f"**Article Stats:**\n- Words: {result['word_count']}\n- Keywords incorporated: {len([k for k, v in result['keyword_counts'].items() if v > 0])}/{len(result['keyword_counts'])}" return result["article"], status, info else: return "", f"❌ Error: {result['error']}", "" def edit_article( self, current_article: str, context: str, keywords: str, edit_instructions: str, model: str ) -> Tuple[str, str]: """Edit existing article based on instructions""" if not current_article.strip(): return current_article, "No article to edit. Please generate an article first." if not edit_instructions.strip(): return current_article, "Please provide editing instructions" result = self.generator.generate_article( context, keywords, model, previous_article=current_article, edit_instructions=edit_instructions ) if result["success"]: keyword_info = "\n".join([f"- {k}: {v} occurrences" for k, v in result["keyword_counts"].items()]) status = f"✅ Article edited successfully!\n\nWord count: {result['word_count']}\n\nKeyword usage:\n{keyword_info}" return result["article"], status else: return current_article, f"❌ Error: {result['error']}" def create_interface(): """Create the Gradio interface""" app = SEOArticleBot() with gr.Blocks(title="SEO Article Generator", theme=gr.themes.Default()) as demo: gr.Markdown(""" # 📝 SEO Article Generator Bot Generate SEO-optimized articles based on your context and keywords. The bot uses reference articles to match writing style and quality. """) with gr.Row(): # Left Column - Inputs with gr.Column(scale=1): gr.Markdown("### 📋 Article Configuration") context_input = gr.Textbox( label="Article Context", placeholder="Provide the main topic, key points, and any specific information you want the article to cover...", lines=10 ) keywords_input = gr.Textbox( label="SEO Keywords (comma-separated)", placeholder="e.g., digital marketing, SEO tips, content strategy, online presence, search rankings", lines=2 ) model_dropdown = gr.Dropdown( choices=list(Config.AI_MODELS.keys()), value=Config.DEFAULT_MODEL, label="AI Model" ) generate_btn = gr.Button( "🚀 Generate Article", variant="primary", size="lg" ) # Status and info section status_output = gr.Textbox( label="Status", interactive=False, lines=6 ) article_info = gr.Markdown("", visible=True) # Right Column - Article Display and Editing with gr.Column(scale=2): gr.Markdown("### 📄 Generated Article") article_display = gr.Markdown( value="*Your generated article will appear here...*", elem_classes=["article-display"] ) # Hidden textbox to store the raw article for editing article_storage = gr.Textbox(visible=False) # Editing section with gr.Group(): gr.Markdown("### ✏️ Edit Article") edit_instructions = gr.Textbox( label="Editing Instructions", placeholder="e.g., Make the introduction more engaging, add more statistics in the second section, expand on the benefits...", lines=3 ) edit_btn = gr.Button( "🔄 Apply Edits", variant="secondary" ) # Footer with instructions gr.Markdown(""" --- **💡 Tips for best results:** - Provide detailed context about your topic and target audience - Include 3-5 relevant SEO keywords (the bot will use at least 2-3) - Use the edit feature to refine specific sections - Reference articles in `articles.txt` are used to match writing style """) # Event handlers def generate_handler(context, keywords, model): article, status, info = app.generate_article(context, keywords, model) if article: return gr.update(value=article), article, status, gr.update(value=info) else: return gr.update(value="*Article generation failed. Check the status message.*"), "", status, gr.update(value="") def edit_handler(current_article, context, keywords, edit_instructions, model): edited_article, status = app.edit_article( current_article, context, keywords, edit_instructions, model ) return gr.update(value=edited_article), edited_article, status # Wire up events generate_btn.click( fn=generate_handler, inputs=[context_input, keywords_input, model_dropdown], outputs=[article_display, article_storage, status_output, article_info] ) edit_btn.click( fn=edit_handler, inputs=[article_storage, context_input, keywords_input, edit_instructions, model_dropdown], outputs=[article_display, article_storage, status_output] ) # Add custom CSS demo.css = """ .article-display { max-height: 600px; overflow-y: auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px; background-color: #ffffff; line-height: 1.6; } .article-display h2 { color: #1f2937; margin-top: 24px; margin-bottom: 16px; } .article-display h3 { color: #374151; margin-top: 20px; margin-bottom: 12px; } .article-display p { margin-bottom: 16px; color: #4b5563; } .article-display ul, .article-display ol { margin-bottom: 16px; padding-left: 24px; } .article-display li { margin-bottom: 8px; } """ return demo if __name__ == "__main__": # Check for required files and keys if not os.path.exists(".env"): print("Warning: .env file not found. Please create one with:") print("OPENAI_API_KEY=your_openai_key") if not os.path.exists(Config.ARTICLES_FILE_PATH): print(f"Warning: {Config.ARTICLES_FILE_PATH} not found.") print("Creating a sample articles.txt file...") # Create a sample articles.txt file sample_content = """=== Article 1: The Ultimate Guide to Content Marketing === Content marketing has become the cornerstone of digital marketing strategies... [Add your reference articles here] === Article 2: SEO Best Practices for 2024 === Search engine optimization continues to evolve... [Add more articles as needed] """ with open(Config.ARTICLES_FILE_PATH, 'w', encoding='utf-8') as f: f.write(sample_content) print(f"Created {Config.ARTICLES_FILE_PATH}. Please add your reference articles to this file.") # Launch the app demo = create_interface() demo.launch( share=True, debug=True, server_name="0.0.0.0", server_port=7862 )