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 |
+
)
|