Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import requests | |
| import json | |
| import pandas as pd | |
| import numpy as np | |
| import re | |
| from bs4 import BeautifulSoup | |
| from datetime import datetime | |
| # ------------------ Utilities ------------------ | |
| def fetch_html(url, timeout=15): | |
| try: | |
| headers = {"User-Agent": "Mozilla/5.0 (compatible; LocalSEO/1.0; +https://example.com)"} | |
| r = requests.get(url, headers=headers, timeout=timeout) | |
| if r.status_code == 200: | |
| return r.text | |
| except Exception: | |
| return "" | |
| return "" | |
| def extract_tag(soup, selector): | |
| el = soup.select_one(selector) | |
| return el.get_text(strip=True) if el else "" | |
| def has_viewport(soup): | |
| return soup.find("meta", attrs={"name": "viewport"}) is not None | |
| def detect_schema_jsonld(soup): | |
| schemas = [] | |
| for tag in soup.find_all("script", type="application/ld+json"): | |
| try: | |
| data = json.loads(tag.string or "{}") | |
| schemas.append(data) | |
| except Exception: | |
| continue | |
| return schemas | |
| def find_phone_and_address(text): | |
| phone_match = re.search(r'(\+?\d[\d\-\s\(\)]{7,}\d)', text) | |
| address_match = re.search(r'(\d{1,4}\s+[A-Za-z0-9\.\-\,\s]+(?:Street|St|Road|Rd|Avenue|Ave|Boulevard|Blvd|Lane|Ln|Town|City|Area|Block|Sector|Phase|Colony).{0,40})', | |
| text, flags=re.IGNORECASE) | |
| phone = phone_match.group(1).strip() if phone_match else "" | |
| address = address_match.group(1).strip() if address_match else "" | |
| return phone, address | |
| def grade_to_badge(score): | |
| if score >= 90: return ("A+", "π") | |
| if score >= 80: return ("A", "β ") | |
| if score >= 70: return ("B", "π") | |
| if score >= 60: return ("C", "π ") | |
| if score >= 50: return ("D", "β οΈ") | |
| return ("F", "β") | |
| def geocode_nominatim(place): | |
| try: | |
| r = requests.get("https://nominatim.openstreetmap.org/search", | |
| params={"q": place, "format": "json", "limit": 1}, | |
| headers={"User-Agent": "LocalSEO-Toolkit/1.0"}, | |
| timeout=20) | |
| if r.status_code == 200 and r.json(): | |
| j = r.json()[0] | |
| return float(j["lat"]), float(j["lon"]), j.get("display_name", "") | |
| except Exception: | |
| pass | |
| return None, None, "" | |
| def call_pagespeed(url, api_key): | |
| endpoint = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed" | |
| try: | |
| r = requests.get(endpoint, params={"url": url, "key": api_key, "strategy": "mobile"}, timeout=30) | |
| if r.status_code == 200: | |
| return r.json() | |
| else: | |
| return {"error": r.text} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def call_serper_local(q, api_key, gl="pk", hl="en"): | |
| url = "https://google.serper.dev/search" | |
| headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} | |
| payload = {"q": q, "gl": gl, "hl": hl, "autocorrect": True} | |
| try: | |
| resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=30) | |
| if resp.status_code == 200: | |
| return resp.json() | |
| return {"error": resp.text} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def sentiment_scores(texts): | |
| try: | |
| from nltk.sentiment import SentimentIntensityAnalyzer | |
| import nltk | |
| try: | |
| nltk.data.find('sentiment/vader_lexicon.zip') | |
| except LookupError: | |
| nltk.download('vader_lexicon') | |
| sia = SentimentIntensityAnalyzer() | |
| scores = [sia.polarity_scores(t)["compound"] for t in texts] | |
| return scores | |
| except Exception: | |
| return [0.0 for _ in texts] | |
| # ------------------ Main Analyzer ------------------ | |
| def analyze( | |
| business_name, primary_domain, target_city, category, nap_phone, nap_address, serper_key, psi_key | |
| ): | |
| html = fetch_html(primary_domain) | |
| if not html: | |
| return f"β Could not fetch {primary_domain}" | |
| soup = BeautifulSoup(html, "html.parser") | |
| title = extract_tag(soup, "title") | |
| meta_desc = extract_tag(soup, "meta[name='description']") | |
| viewport = "Yes β " if has_viewport(soup) else "No β" | |
| phone, address = find_phone_and_address(html) | |
| # PageSpeed | |
| psi_score = None | |
| if psi_key: | |
| psi = call_pagespeed(primary_domain, psi_key) | |
| psi_score = psi.get("lighthouseResult", {}).get("categories", {}).get("performance", {}).get("score") | |
| if psi_score is not None: | |
| psi_score = int(psi_score * 100) | |
| # Serper local | |
| serper_results = None | |
| if serper_key: | |
| serper_results = call_serper_local(business_name + " " + target_city, serper_key) | |
| # Report assembly | |
| report_md = f""" | |
| # π©Ί Doctor of Local SEO Report | |
| **Business:** {business_name} | |
| **Website:** {primary_domain} | |
| **Target City:** {target_city} | |
| **Category:** {category} | |
| --- | |
| ### On-page Info | |
| - Title: {title} | |
| - Meta Description: {meta_desc} | |
| - Viewport tag: {viewport} | |
| ### NAP (detected vs provided) | |
| - Provided Phone: {nap_phone} | |
| - Detected Phone: {phone or "Not found"} | |
| - Provided Address: {nap_address} | |
| - Detected Address: {address or "Not found"} | |
| """ | |
| if psi_score is not None: | |
| grade, emoji = grade_to_badge(psi_score) | |
| report_md += f"\n\n### PageSpeed Insights\n- Score: {psi_score} ({grade} {emoji})" | |
| if serper_results and "organic" in serper_results: | |
| top_results = [r.get("title", "") for r in serper_results["organic"][:3]] | |
| report_md += f"\n\n### Top Local SERP Results\n" + "\n".join([f"- {t}" for t in top_results]) | |
| report_md += f"\n\n_(Generated {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')})_" | |
| return report_md | |
| # ------------------ Gradio Interface ------------------ | |
| iface = gr.Interface( | |
| fn=analyze, | |
| inputs=[ | |
| gr.Textbox(label="Business Name", value="Sample Dental Clinic"), | |
| gr.Textbox(label="Website URL", value="https://example.com"), | |
| gr.Textbox(label="Target City/Area", value="Karachi, Pakistan"), | |
| gr.Dropdown(label="Category (OSM)", choices=["shop", "amenity", "office", "tourism", "healthcare"], value="shop"), | |
| gr.Textbox(label="Your Phone (for NAP check)", value="+92 300 0000000"), | |
| gr.Textbox(label="Your Address (for NAP check)", value="123 Main Street, Karachi"), | |
| gr.Textbox(label="Serper.dev API Key (optional)", type="password"), | |
| gr.Textbox(label="Google PSI API Key (optional)", type="password"), | |
| ], | |
| outputs=gr.Markdown(label="SEO Audit Report"), | |
| title="Doctor of Local SEO β Gradio Edition", | |
| description="All-in-one Local SEO toolkit for quick audits. Supports PSI & Serper integrations.", | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() | |