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
)