Spaces:
Sleeping
Sleeping
Update seo_analyzer_ui.py
Browse files- seo_analyzer_ui.py +279 -9
seo_analyzer_ui.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
SEO Analyzer UI
|
| 3 |
"""
|
| 4 |
-
|
| 5 |
import gradio as gr
|
| 6 |
import logging
|
| 7 |
import json
|
|
@@ -19,6 +18,9 @@ from concurrent.futures import ThreadPoolExecutor
|
|
| 19 |
from datetime import datetime
|
| 20 |
import tempfile
|
| 21 |
|
|
|
|
|
|
|
|
|
|
| 22 |
from crawler import Crawler
|
| 23 |
from frontier import URLFrontier
|
| 24 |
from models import URL, Page
|
|
@@ -28,7 +30,6 @@ from dotenv import load_dotenv, find_dotenv
|
|
| 28 |
|
| 29 |
load_dotenv(find_dotenv())
|
| 30 |
|
| 31 |
-
# Check if we're in deployment mode (e.g., Hugging Face Spaces)
|
| 32 |
IS_DEPLOYMENT = os.getenv('DEPLOYMENT', 'false').lower() == 'true'
|
| 33 |
|
| 34 |
# Custom CSS for better styling
|
|
@@ -694,15 +695,284 @@ def create_ui() -> gr.Interface:
|
|
| 694 |
|
| 695 |
return iface
|
| 696 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
if __name__ == "__main__":
|
| 698 |
-
# Create base storage directory if it doesn't exist
|
| 699 |
os.makedirs(config.STORAGE_PATH, exist_ok=True)
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
|
|
|
|
|
|
| 704 |
share=False,
|
| 705 |
server_name="0.0.0.0",
|
| 706 |
show_api=False,
|
| 707 |
show_error=True,
|
| 708 |
-
)
|
|
|
|
| 1 |
"""
|
| 2 |
+
SEO Analyzer UI with Auto Ad Generator Tab
|
| 3 |
"""
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
import logging
|
| 6 |
import json
|
|
|
|
| 18 |
from datetime import datetime
|
| 19 |
import tempfile
|
| 20 |
|
| 21 |
+
from bs4 import BeautifulSoup
|
| 22 |
+
import requests
|
| 23 |
+
|
| 24 |
from crawler import Crawler
|
| 25 |
from frontier import URLFrontier
|
| 26 |
from models import URL, Page
|
|
|
|
| 30 |
|
| 31 |
load_dotenv(find_dotenv())
|
| 32 |
|
|
|
|
| 33 |
IS_DEPLOYMENT = os.getenv('DEPLOYMENT', 'false').lower() == 'true'
|
| 34 |
|
| 35 |
# Custom CSS for better styling
|
|
|
|
| 695 |
|
| 696 |
return iface
|
| 697 |
|
| 698 |
+
# ----- SEO Analyzer UI (as a function) -----
|
| 699 |
+
def seo_analyzer_ui():
|
| 700 |
+
def analyze(url: str, api_key: str, max_pages: int, progress: gr.Progress = gr.Progress()) -> Tuple[str, str]:
|
| 701 |
+
try:
|
| 702 |
+
analyzer = SEOAnalyzer(api_key)
|
| 703 |
+
analysis, _, logs = analyzer.analyze_website(url, max_pages, progress)
|
| 704 |
+
log_output = ""
|
| 705 |
+
while not analyzer.log_queue.empty():
|
| 706 |
+
try:
|
| 707 |
+
log_output += analyzer.log_queue.get_nowait() + "\n"
|
| 708 |
+
except queue.Empty:
|
| 709 |
+
break
|
| 710 |
+
progress(1.0, "Analysis complete")
|
| 711 |
+
return analysis, log_output
|
| 712 |
+
except Exception as e:
|
| 713 |
+
error_msg = f"Error: {str(e)}"
|
| 714 |
+
logger.error(error_msg)
|
| 715 |
+
return error_msg, error_msg
|
| 716 |
+
|
| 717 |
+
about_markdown = """
|
| 718 |
+
# 🔍 SEO Analyzer Pro
|
| 719 |
+
Analyze your website's SEO performance using advanced crawling and AI technology.
|
| 720 |
+
...
|
| 721 |
+
"""
|
| 722 |
+
with gr.Blocks() as seo_tab:
|
| 723 |
+
gr.Markdown(about_markdown)
|
| 724 |
+
with gr.Row():
|
| 725 |
+
with gr.Column(scale=2):
|
| 726 |
+
with gr.Group(elem_classes="input-section"):
|
| 727 |
+
gr.Markdown("### 📝 Enter Website Details")
|
| 728 |
+
url_input = gr.Textbox(
|
| 729 |
+
label="Website URL",
|
| 730 |
+
placeholder="https://example.com",
|
| 731 |
+
elem_classes="input-container",
|
| 732 |
+
info="Enter the full URL of the website you want to analyze (e.g., https://example.com)"
|
| 733 |
+
)
|
| 734 |
+
api_key = gr.Textbox(
|
| 735 |
+
label="OpenAI API Key",
|
| 736 |
+
placeholder="sk-...",
|
| 737 |
+
type="password",
|
| 738 |
+
elem_classes="input-container",
|
| 739 |
+
info="Your OpenAI API key is required for AI-powered analysis. Keep this secure!"
|
| 740 |
+
)
|
| 741 |
+
max_pages = gr.Slider(
|
| 742 |
+
minimum=1,
|
| 743 |
+
maximum=50,
|
| 744 |
+
value=10,
|
| 745 |
+
step=1,
|
| 746 |
+
label="Maximum Pages to Crawl",
|
| 747 |
+
elem_classes="slider-container",
|
| 748 |
+
info="Choose how many pages to analyze. More pages = more comprehensive analysis but takes longer"
|
| 749 |
+
)
|
| 750 |
+
analyze_btn = gr.Button(
|
| 751 |
+
"🔍 Analyze Website",
|
| 752 |
+
elem_classes="primary-button"
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
with gr.Row():
|
| 756 |
+
with gr.Column():
|
| 757 |
+
with gr.Group(elem_classes="analysis-section"):
|
| 758 |
+
gr.Markdown("### 📊 Analysis Results")
|
| 759 |
+
analysis_output = gr.Markdown(
|
| 760 |
+
label="SEO Analysis",
|
| 761 |
+
elem_classes="markdown-output"
|
| 762 |
+
)
|
| 763 |
+
with gr.Row():
|
| 764 |
+
with gr.Column():
|
| 765 |
+
with gr.Group(elem_classes="log-section"):
|
| 766 |
+
gr.Markdown("### 📋 Process Logs")
|
| 767 |
+
logs_output = gr.Textbox(
|
| 768 |
+
label="Logs",
|
| 769 |
+
lines=10,
|
| 770 |
+
elem_classes="log-output"
|
| 771 |
+
)
|
| 772 |
+
analyze_btn.click(
|
| 773 |
+
fn=analyze,
|
| 774 |
+
inputs=[url_input, api_key, max_pages],
|
| 775 |
+
outputs=[analysis_output, logs_output],
|
| 776 |
+
)
|
| 777 |
+
return seo_tab
|
| 778 |
+
|
| 779 |
+
# ---- Auto Ad Generator UI as a function ----
|
| 780 |
+
def auto_ad_generator_ui():
|
| 781 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 782 |
+
|
| 783 |
+
def extract_text_from_url(url):
|
| 784 |
+
try:
|
| 785 |
+
resp = requests.get(url, timeout=30, headers={
|
| 786 |
+
"User-Agent": "Mozilla/5.0 (compatible; Bot/1.0)"
|
| 787 |
+
})
|
| 788 |
+
soup = BeautifulSoup(resp.content, "html.parser")
|
| 789 |
+
candidates = soup.find_all(['h1','h2','h3','h4','p','span','li'])
|
| 790 |
+
text = ' '.join([c.get_text(strip=True) for c in candidates])
|
| 791 |
+
text = text[:4000]
|
| 792 |
+
if len(text) < 100:
|
| 793 |
+
raise ValueError("Could not extract enough content (site may require JavaScript). Please enter keywords manually.")
|
| 794 |
+
return text
|
| 795 |
+
except Exception as e:
|
| 796 |
+
raise ValueError(f"URL extraction error: {e}")
|
| 797 |
+
|
| 798 |
+
def extract_keywords(text):
|
| 799 |
+
prompt = f"""
|
| 800 |
+
Extract up to 10 concise, relevant SEO keywords suitable for an automotive advertisement from the following content:
|
| 801 |
+
{text}
|
| 802 |
+
Keywords:
|
| 803 |
+
"""
|
| 804 |
+
response = openai.ChatCompletion.create(
|
| 805 |
+
model="gpt-4",
|
| 806 |
+
messages=[{"role": "user", "content": prompt}],
|
| 807 |
+
temperature=0.6,
|
| 808 |
+
max_tokens=100
|
| 809 |
+
)
|
| 810 |
+
output = response.choices[0].message.content.strip()
|
| 811 |
+
if ',' in output:
|
| 812 |
+
keywords = output.split(',')
|
| 813 |
+
else:
|
| 814 |
+
keywords = output.split('\n')
|
| 815 |
+
return [kw.strip() for kw in keywords if kw.strip()]
|
| 816 |
+
|
| 817 |
+
def generate_ad_copy(platform, keywords):
|
| 818 |
+
prompt = f"""
|
| 819 |
+
Create a compelling, SEO-optimized {platform} ad using these keywords: {', '.join(keywords)}.
|
| 820 |
+
Include a clear and enticing call-to-action.
|
| 821 |
+
"""
|
| 822 |
+
response = openai.ChatCompletion.create(
|
| 823 |
+
model="gpt-4",
|
| 824 |
+
messages=[{"role": "user", "content": prompt}],
|
| 825 |
+
temperature=0.7,
|
| 826 |
+
max_tokens=300
|
| 827 |
+
)
|
| 828 |
+
return response.choices[0].message.content.strip()
|
| 829 |
+
|
| 830 |
+
def generate_ad_image(keywords):
|
| 831 |
+
kw_str = ", ".join(keywords)
|
| 832 |
+
image_prompt = (
|
| 833 |
+
f"High-quality, photorealistic automotive ad photo of a luxury car. "
|
| 834 |
+
f"Clean background, professional lighting, stylish dealership setting. "
|
| 835 |
+
f"Keywords: {kw_str}. Room for text overlay, wide format, visually appealing."
|
| 836 |
+
)
|
| 837 |
+
response = openai.Image.create(
|
| 838 |
+
prompt=image_prompt,
|
| 839 |
+
n=1,
|
| 840 |
+
size="512x512"
|
| 841 |
+
)
|
| 842 |
+
image_url = response["data"][0]["url"]
|
| 843 |
+
img_data = requests.get(image_url).content
|
| 844 |
+
img_file = "generated_ad_image.png"
|
| 845 |
+
with open(img_file, "wb") as f:
|
| 846 |
+
f.write(img_data)
|
| 847 |
+
return img_file
|
| 848 |
+
|
| 849 |
+
def platform_html(platform, ad_text):
|
| 850 |
+
if platform == "Facebook":
|
| 851 |
+
color = "#1877F2"
|
| 852 |
+
icon = "🌐"
|
| 853 |
+
elif platform == "Instagram":
|
| 854 |
+
color = "linear-gradient(90deg, #f58529 0%, #dd2a7b 50%, #8134af 100%)"
|
| 855 |
+
icon = "📸"
|
| 856 |
+
elif platform == "X (Twitter)":
|
| 857 |
+
color = "#14171A"
|
| 858 |
+
icon = "🐦"
|
| 859 |
+
else: # Google Search
|
| 860 |
+
color = "#4285F4"
|
| 861 |
+
icon = "🔍"
|
| 862 |
+
|
| 863 |
+
if platform == "Instagram":
|
| 864 |
+
content = f"""
|
| 865 |
+
<div style="background: {color}; padding: 2px; border-radius: 12px; margin-bottom:16px;">
|
| 866 |
+
<div style="background: white; color: #333; padding: 18px 20px; border-radius: 10px;">
|
| 867 |
+
<span style="font-size: 1.5em;">{icon} <b>{platform}</b></span>
|
| 868 |
+
<div style="margin-top: 12px; font-size: 1.1em; line-height:1.6;">{ad_text}</div>
|
| 869 |
+
</div>
|
| 870 |
+
</div>
|
| 871 |
+
"""
|
| 872 |
+
else:
|
| 873 |
+
content = f"""
|
| 874 |
+
<div style="background: {color}; color: white; padding: 18px 20px; border-radius: 12px; margin-bottom:16px; min-height: 120px;">
|
| 875 |
+
<span style="font-size: 1.5em;">{icon} <b>{platform}</b></span>
|
| 876 |
+
<div style="margin-top: 12px; font-size: 1.1em; line-height:1.6;">{ad_text}</div>
|
| 877 |
+
</div>
|
| 878 |
+
"""
|
| 879 |
+
return content
|
| 880 |
+
|
| 881 |
+
def main_workflow(input_mode, url_or_keywords):
|
| 882 |
+
error = None
|
| 883 |
+
keywords = []
|
| 884 |
+
ad_copies = {}
|
| 885 |
+
image_path = None
|
| 886 |
+
|
| 887 |
+
if input_mode == "URL":
|
| 888 |
+
try:
|
| 889 |
+
text = extract_text_from_url(url_or_keywords)
|
| 890 |
+
keywords = extract_keywords(text)
|
| 891 |
+
except Exception as e:
|
| 892 |
+
return None, None, None, f"{e}"
|
| 893 |
+
else:
|
| 894 |
+
keywords = [kw.strip() for kw in url_or_keywords.split(",") if kw.strip()]
|
| 895 |
+
if not keywords:
|
| 896 |
+
return None, None, None, "Please provide at least one keyword."
|
| 897 |
+
# Generate ad copies
|
| 898 |
+
platforms = ["Facebook", "Instagram", "X (Twitter)", "Google Search"]
|
| 899 |
+
for platform in platforms:
|
| 900 |
+
ad_copies[platform] = generate_ad_copy(platform, keywords)
|
| 901 |
+
# Generate image
|
| 902 |
+
try:
|
| 903 |
+
image_path = generate_ad_image(keywords)
|
| 904 |
+
except Exception as e:
|
| 905 |
+
error = f"Image generation error: {e}"
|
| 906 |
+
|
| 907 |
+
# Save ads to txt
|
| 908 |
+
output_txt = "generated_ads.txt"
|
| 909 |
+
with open(output_txt, "w", encoding="utf-8") as f:
|
| 910 |
+
for platform, content in ad_copies.items():
|
| 911 |
+
f.write(f"--- {platform} Ad Copy ---\n{content}\n\n")
|
| 912 |
+
return keywords, ad_copies, image_path, error
|
| 913 |
+
|
| 914 |
+
def run_space(input_mode, url, keywords):
|
| 915 |
+
url_or_keywords = url if input_mode == "URL" else keywords
|
| 916 |
+
keywords, ad_copies, image_path, error = main_workflow(input_mode, url_or_keywords)
|
| 917 |
+
ad_previews = ""
|
| 918 |
+
if ad_copies:
|
| 919 |
+
for platform, ad in ad_copies.items():
|
| 920 |
+
ad_previews += platform_html(platform, ad)
|
| 921 |
+
return (
|
| 922 |
+
keywords,
|
| 923 |
+
ad_previews,
|
| 924 |
+
image_path,
|
| 925 |
+
"generated_ads.txt" if ad_copies else None,
|
| 926 |
+
error
|
| 927 |
+
)
|
| 928 |
+
|
| 929 |
+
with gr.Blocks() as ad_tab:
|
| 930 |
+
gr.Markdown("# 🚗 Auto Ad Generator\nPaste a car listing URL **or** enter your own keywords, then preview AI-generated ads for each social media platform, plus an auto-generated image!")
|
| 931 |
+
input_mode = gr.Radio(["URL", "Keywords"], value="URL", label="Input Type")
|
| 932 |
+
url_input = gr.Textbox(label="Listing URL", placeholder="https://www.cars.com/listing/...", visible=True)
|
| 933 |
+
kw_input = gr.Textbox(label="Manual Keywords (comma separated)", placeholder="e.g. BMW, used car, sunroof", visible=False)
|
| 934 |
+
submit_btn = gr.Button("Generate Ads")
|
| 935 |
+
|
| 936 |
+
gr.Markdown("## Keywords")
|
| 937 |
+
kw_out = gr.JSON(label="Extracted/Provided Keywords")
|
| 938 |
+
|
| 939 |
+
gr.Markdown("## Ad Copy Previews")
|
| 940 |
+
ad_out = gr.HTML(label="Ad Copy Preview")
|
| 941 |
+
|
| 942 |
+
gr.Markdown("## Generated Ad Image")
|
| 943 |
+
img_out = gr.Image(label="Generated Ad Image", type="filepath")
|
| 944 |
+
|
| 945 |
+
gr.Markdown("## Download Ad Copies")
|
| 946 |
+
file_out = gr.File(label="Download TXT")
|
| 947 |
+
|
| 948 |
+
err_out = gr.Textbox(label="Errors", interactive=False)
|
| 949 |
+
|
| 950 |
+
def show_hide_fields(choice):
|
| 951 |
+
return (
|
| 952 |
+
gr.update(visible=choice == "URL"),
|
| 953 |
+
gr.update(visible=choice == "Keywords"),
|
| 954 |
+
)
|
| 955 |
+
|
| 956 |
+
input_mode.change(show_hide_fields, input_mode, [url_input, kw_input])
|
| 957 |
+
|
| 958 |
+
submit_btn.click(
|
| 959 |
+
run_space,
|
| 960 |
+
inputs=[input_mode, url_input, kw_input],
|
| 961 |
+
outputs=[kw_out, ad_out, img_out, file_out, err_out]
|
| 962 |
+
)
|
| 963 |
+
return ad_tab
|
| 964 |
+
|
| 965 |
+
# ---- Main App: Two Tabs ----
|
| 966 |
if __name__ == "__main__":
|
|
|
|
| 967 |
os.makedirs(config.STORAGE_PATH, exist_ok=True)
|
| 968 |
+
with gr.Blocks(css=CUSTOM_CSS) as demo:
|
| 969 |
+
with gr.Tab("SEO Analyzer"):
|
| 970 |
+
seo_analyzer_ui()
|
| 971 |
+
with gr.Tab("Auto Ad Generator"):
|
| 972 |
+
auto_ad_generator_ui()
|
| 973 |
+
demo.launch(
|
| 974 |
share=False,
|
| 975 |
server_name="0.0.0.0",
|
| 976 |
show_api=False,
|
| 977 |
show_error=True,
|
| 978 |
+
)
|