Spaces:
Running on Zero
Running on Zero
| import gradio as gr | |
| import asyncio | |
| import subprocess | |
| import sys | |
| import os | |
| from datetime import datetime | |
| os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", "/home/user/.cache/ms-playwright") | |
| def install_playwright_browsers(): | |
| marker = os.path.join(os.environ["PLAYWRIGHT_BROWSERS_PATH"], ".installed") | |
| if os.path.exists(marker): | |
| print("β Playwright browsers already installed, skipping.") | |
| return | |
| try: | |
| print("π¦ Installing Playwright browsers...") | |
| subprocess.check_call( | |
| [sys.executable, "-m", "playwright", "install", "chromium"], | |
| ) | |
| os.makedirs(os.environ["PLAYWRIGHT_BROWSERS_PATH"], exist_ok=True) | |
| open(marker, "w").close() | |
| print("β Playwright browsers installed successfully.") | |
| except subprocess.CalledProcessError as e: | |
| print(f"β Playwright install failed with code {e.returncode}") | |
| except Exception as e: | |
| print(f"β Unexpected error during install: {e}") | |
| install_playwright_browsers() | |
| try: | |
| import spaces | |
| HAS_SPACES = True | |
| except ImportError: | |
| HAS_SPACES = False | |
| if HAS_SPACES: | |
| def dummy_gpu(): | |
| pass | |
| from seo_analyzer import run_seo_analysis_fastapi | |
| from ai_visibility import run_ai_visibility_analysis | |
| # ---- Helper functions to format output ---- | |
| def format_seo_result(result): | |
| if isinstance(result, tuple): | |
| data, csv_path = result | |
| if not data: | |
| return "No data returned." | |
| output = f"## SEO Analysis Results\n" | |
| output += f"**Pages analyzed:** {len(data)}\n" | |
| scores = [p.get('seo_score', 0) for p in data] | |
| avg = sum(scores) / len(scores) if scores else 0 | |
| output += f"**Average SEO Score:** {avg:.1f}/100\n\n" | |
| # Generate detailed strengths and issues based on page metrics | |
| detailed_strengths = [] | |
| detailed_issues = [] | |
| for page in data: | |
| url = page.get('url', '') | |
| # Title | |
| title_len = len(page.get('title', '')) | |
| if 50 <= title_len <= 60: | |
| detailed_strengths.append(f"- Strong title length ({title_len} chars) on {url}") | |
| elif title_len < 30 or title_len > 70: | |
| detailed_issues.append(f"- Title too {'short' if title_len < 30 else 'long'} ({title_len} chars) on {url}") | |
| # Meta description | |
| meta_len = len(page.get('meta_description', '')) | |
| if 120 <= meta_len <= 160: | |
| detailed_strengths.append(f"- Good meta description length ({meta_len} chars) on {url}") | |
| elif meta_len > 0 and (meta_len < 70 or meta_len > 170): | |
| detailed_issues.append(f"- Meta description length ({meta_len} chars) suboptimal on {url}") | |
| # H1 | |
| h1 = page.get('h1_count', 0) | |
| if h1 == 1: | |
| detailed_strengths.append(f"- Exactly one H1 on {url}") | |
| elif h1 == 0: | |
| detailed_issues.append(f"- Missing H1 on {url}") | |
| elif h1 > 1: | |
| detailed_issues.append(f"- Multiple H1s ({h1}) on {url}") | |
| # Word count | |
| wc = page.get('word_count', 0) | |
| if wc >= 800: | |
| detailed_strengths.append(f"- Good word count ({wc}) on {url}") | |
| elif wc < 300: | |
| detailed_issues.append(f"- Low word count ({wc}) on {url}") | |
| # Alt tags | |
| total_img = page.get('total_images', 0) | |
| missing_alt = page.get('missing_alt_tags', 0) | |
| if total_img > 0 and missing_alt == 0: | |
| detailed_strengths.append(f"- All images have alt text on {url}") | |
| elif total_img > 0 and missing_alt > 0: | |
| detailed_issues.append(f"- {missing_alt} images missing alt text on {url}") | |
| # Schema | |
| schema = page.get('schema_types', '') | |
| if schema and schema != "No schema found": | |
| detailed_strengths.append(f"- Schema detected ({schema}) on {url}") | |
| else: | |
| detailed_issues.append(f"- No schema found on {url}") | |
| # Readability | |
| readability = page.get('readability_score', 0) | |
| if readability >= 50: | |
| detailed_strengths.append(f"- Good readability score ({readability}) on {url}") | |
| elif readability < 30: | |
| detailed_issues.append(f"- Poor readability ({readability}) on {url}") | |
| if detailed_strengths: | |
| output += "### SEO Strengths (detailed)\n" | |
| output += "\n".join(detailed_strengths) + "\n\n" | |
| if detailed_issues: | |
| output += "### SEO Issues (detailed)\n" | |
| output += "\n".join(detailed_issues) + "\n\n" | |
| # Per-page data | |
| for i, page in enumerate(data, 1): | |
| output += f"### Page {i}: {page.get('url', '')}\n" | |
| output += f"- Score: {page.get('seo_score', 0)}/100\n" | |
| output += f"- Title: {page.get('title', 'No title')}\n" | |
| output += f"- Word Count: {page.get('word_count', 0)}\n" | |
| output += f"- H1: {page.get('h1_count', 0)}, H2: {page.get('h2_count', 0)}, H3: {page.get('h3_count', 0)}\n" | |
| output += f"- Images: {page.get('total_images', 0)} (missing alt: {page.get('missing_alt_tags', 0)})\n" | |
| output += f"- Internal/External links: {page.get('internal_links', 0)}/{page.get('external_links', 0)}\n" | |
| output += f"- Readability: {page.get('readability_score', 0)}\n" | |
| output += f"- Grammar Errors: {page.get('grammar_errors', 0)}\n" | |
| output += f"- Canonical Tag: {'Yes' if page.get('canonical_tag') else 'No'}\n" | |
| output += f"- OpenGraph Tags: {page.get('opengraph_tags', 0)}\n" | |
| output += f"- Twitter Cards: {page.get('twitter_tags', 0)}\n" | |
| output += f"- Robots Meta: {page.get('robots_meta', 'none')}\n" | |
| output += f"- Viewport: {'Yes' if page.get('viewport_present') else 'No'}\n" | |
| output += f"- Schema Types: {page.get('schema_types', 'none')}\n" | |
| output += f"- Text/HTML Ratio: {page.get('text_to_html_ratio', 0)}%\n" | |
| output += f"- Load Time: {page.get('load_time', 0)}s\n" | |
| output += f"- Meta Description: {page.get('meta_description', '')}\n" | |
| output += f"- Heading Order: {page.get('heading_order', '')}\n" | |
| if page.get('ai_suggestions'): | |
| output += f"- AI Suggestions: {page['ai_suggestions'][:200]}...\n" | |
| output += "\n" | |
| return output | |
| else: | |
| return f"β Error: {result.get('message', 'Unknown error')}" | |
| def format_ai_result(result): | |
| if result.get('status') == 'error': | |
| return f"β Error: {result.get('message', 'Unknown error')}" | |
| output = f"## AI Visibility / Readiness Analysis\n" | |
| output += f"**URL:** {result.get('url', '')}\n" | |
| output += f"**Pages analyzed:** {result.get('pages_analyzed', 0)}\n" | |
| output += f"**Overall AI Readiness Score:** {result.get('ai_readiness_score', 0)}/100\n" | |
| output += f"**Page types detected:** {result.get('page_type_breakdown', {})}\n\n" | |
| cat_scores = result.get('category_scores', {}) | |
| if cat_scores: | |
| output += "### Category Scores\n" | |
| for k, v in cat_scores.items(): | |
| output += f"- {k.replace('_score', '').replace('_', ' ').title()}: {v if v is not None else 'N/A'}\n" | |
| output += "\n" | |
| previews = result.get('results_preview', []) | |
| if previews: | |
| output += "### Per-Page Details\n" | |
| for p in previews: | |
| output += f"**URL:** {p.get('url', '')}\n" | |
| output += f"- Page Type: {p.get('page_type', 'unknown')} (conf: {p.get('page_type_confidence', 0):.2f})\n" | |
| output += f"- Readiness Score: {p.get('ai_readiness_score', 0)}/100\n" | |
| output += f"- Topic Clarity: {p.get('topic_clarity', 0)}\n" | |
| output += f"- Content Completeness: {p.get('content_completeness', 0)}\n" | |
| output += f"- Entity Clarity: {p.get('entity_clarity', 'N/A')}\n" | |
| output += f"- Freshness: {p.get('freshness_status', 'unknown')}\n\n" | |
| # Detailed issues and strengths (per page) from backend | |
| issues = result.get('issues', []) | |
| strengths = result.get('strengths', []) | |
| if issues: | |
| output += "### Detailed Issues (per page)\n" | |
| for issue in issues: | |
| output += f"- {issue.get('title')} (Severity: {issue.get('severity')}) on {issue.get('page')}\n" | |
| output += f" Explanation: {issue.get('explanation')}\n" | |
| if issue.get('recommended_fix'): | |
| output += f" Fix: {issue.get('recommended_fix')}\n" | |
| if strengths: | |
| output += "### Detailed Strengths (per page)\n" | |
| for strength in strengths: | |
| output += f"- {strength.get('title')} on {strength.get('page')}\n" | |
| output += f" Detail: {strength.get('detail')}\n" | |
| return output | |
| # ---- Async analysis wrappers ---- | |
| async def analyze_seo_async(url, max_pages, max_concurrent, use_ai): | |
| result = await run_seo_analysis_fastapi( | |
| base_url=url, | |
| max_pages=int(max_pages), | |
| use_ai=use_ai, | |
| max_concurrent=int(max_concurrent) | |
| ) | |
| return format_seo_result(result) | |
| async def analyze_ai_async(url, max_pages, max_concurrent, use_ai): | |
| result = await run_ai_visibility_analysis( | |
| base_url=url, | |
| max_pages=int(max_pages), | |
| max_concurrent=int(max_concurrent), | |
| use_ai=use_ai | |
| ) | |
| return format_ai_result(result) | |
| # ---- Gradio Interface ---- | |
| with gr.Blocks(title="SEO & AI Visibility Analyzer") as demo: | |
| gr.Markdown("# π SEO & AI Visibility Analysis Tool") | |
| gr.Markdown("Enter a website URL to analyze its SEO health and AI search readiness.") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| url_input = gr.Textbox(label="Website URL", placeholder="https://example.com", value="https://example.com") | |
| with gr.Column(scale=1): | |
| max_pages_input = gr.Number(label="Max Pages", value=3, minimum=1, maximum=20, step=1) | |
| with gr.Column(scale=1): | |
| max_concurrent_input = gr.Number(label="Concurrent Browsers", value=1, minimum=1, maximum=5, step=1) | |
| with gr.Column(scale=1): | |
| use_ai_check = gr.Checkbox(label="Enable AI Suggestions", value=True) | |
| with gr.Row(): | |
| seo_btn = gr.Button("π Analyze SEO", variant="primary") | |
| ai_btn = gr.Button("π€ Analyze AI Visibility", variant="secondary") | |
| output = gr.Markdown(label="Results") | |
| seo_btn.click( | |
| fn=analyze_seo_async, | |
| inputs=[url_input, max_pages_input, max_concurrent_input, use_ai_check], | |
| outputs=output | |
| ) | |
| ai_btn.click( | |
| fn=analyze_ai_async, | |
| inputs=[url_input, max_pages_input, max_concurrent_input, use_ai_check], | |
| outputs=output | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |
| # import gradio as gr | |
| # import asyncio | |
| # import subprocess | |
| # import sys | |
| # import os | |
| # from datetime import datetime | |
| # os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", "/home/user/.cache/ms-playwright") | |
| # def install_playwright_browsers(): | |
| # """Install Playwright Chromium browsers β works on Hugging Face Spaces.""" | |
| # marker = os.path.join(os.environ["PLAYWRIGHT_BROWSERS_PATH"], ".installed") | |
| # if os.path.exists(marker): | |
| # print("β Playwright browsers already installed, skipping.") | |
| # return | |
| # try: | |
| # print("π¦ Installing Playwright browsers...") | |
| # subprocess.check_call( | |
| # [sys.executable, "-m", "playwright", "install", "chromium"], | |
| # ) | |
| # os.makedirs(os.environ["PLAYWRIGHT_BROWSERS_PATH"], exist_ok=True) | |
| # open(marker, "w").close() | |
| # print("β Playwright browsers installed successfully.") | |
| # except subprocess.CalledProcessError as e: | |
| # print(f"β Playwright install failed with code {e.returncode}") | |
| # except Exception as e: | |
| # print(f"β Unexpected error during install: {e}") | |
| # install_playwright_browsers() | |
| # try: | |
| # import spaces | |
| # HAS_SPACES = True | |
| # except ImportError: | |
| # HAS_SPACES = False | |
| # if HAS_SPACES: | |
| # @spaces.GPU | |
| # def dummy_gpu(): | |
| # pass # This makes ZeroGPU happy | |
| # from seo_analyzer import run_seo_analysis_fastapi | |
| # from ai_visibility import run_ai_visibility_analysis | |
| # # ---- Helper functions to format output ---- | |
| # def format_seo_result(result): | |
| # if isinstance(result, tuple): | |
| # data, csv_path = result | |
| # if not data: | |
| # return "No data returned." | |
| # output = f"## SEO Analysis Results\n" | |
| # output += f"**Pages analyzed:** {len(data)}\n" | |
| # scores = [p.get('seo_score', 0) for p in data] | |
| # avg = sum(scores) / len(scores) if scores else 0 | |
| # output += f"**Average SEO Score:** {avg:.1f}/100\n\n" | |
| # for i, page in enumerate(data, 1): | |
| # output += f"### Page {i}: {page.get('url', '')}\n" | |
| # output += f"- Score: {page.get('seo_score', 0)}/100\n" | |
| # output += f"- Title: {page.get('title', 'No title')}\n" | |
| # output += f"- Word Count: {page.get('word_count', 0)}\n" | |
| # output += f"- H1: {page.get('h1_count', 0)}, H2: {page.get('h2_count', 0)}, H3: {page.get('h3_count', 0)}\n" | |
| # output += f"- Images: {page.get('total_images', 0)} (missing alt: {page.get('missing_alt_tags', 0)})\n" | |
| # output += f"- Internal/External links: {page.get('internal_links', 0)}/{page.get('external_links', 0)}\n" | |
| # if page.get('ai_suggestions'): | |
| # output += f"- AI Suggestions: {page['ai_suggestions'][:200]}...\n" | |
| # output += "\n" | |
| # return output | |
| # else: | |
| # return f"β Error: {result.get('message', 'Unknown error')}" | |
| # def format_ai_result(result): | |
| # if result.get('status') == 'error': | |
| # return f"β Error: {result.get('message', 'Unknown error')}" | |
| # output = f"## AI Visibility / Readiness Analysis\n" | |
| # output += f"**URL:** {result.get('url', '')}\n" | |
| # output += f"**Pages analyzed:** {result.get('pages_analyzed', 0)}\n" | |
| # output += f"**Overall AI Readiness Score:** {result.get('ai_readiness_score', 0)}/100\n" | |
| # output += f"**Page types detected:** {result.get('page_type_breakdown', {})}\n\n" | |
| # cat_scores = result.get('category_scores', {}) | |
| # if cat_scores: | |
| # output += "### Category Scores\n" | |
| # for k, v in cat_scores.items(): | |
| # output += f"- {k.replace('_score', '').replace('_', ' ').title()}: {v if v is not None else 'N/A'}\n" | |
| # output += "\n" | |
| # previews = result.get('results_preview', []) | |
| # if previews: | |
| # output += "### Per-Page Details\n" | |
| # for p in previews: | |
| # output += f"**URL:** {p.get('url', '')}\n" | |
| # output += f"- Page Type: {p.get('page_type', 'unknown')} (conf: {p.get('page_type_confidence', 0):.2f})\n" | |
| # output += f"- Readiness Score: {p.get('ai_readiness_score', 0)}/100\n" | |
| # output += f"- Topic Clarity: {p.get('topic_clarity', 0)}\n" | |
| # output += f"- Content Completeness: {p.get('content_completeness', 0)}\n" | |
| # output += f"- Entity Clarity: {p.get('entity_clarity', 'N/A')}\n" | |
| # output += f"- Freshness: {p.get('freshness_status', 'unknown')}\n\n" | |
| # return output | |
| # # ---- Async analysis wrappers (Gradio will handle async functions) ---- | |
| # async def analyze_seo_async(url, max_pages, max_concurrent, use_ai): | |
| # result = await run_seo_analysis_fastapi( | |
| # base_url=url, | |
| # max_pages=int(max_pages), | |
| # use_ai=use_ai, | |
| # max_concurrent=int(max_concurrent) | |
| # ) | |
| # return format_seo_result(result) | |
| # async def analyze_ai_async(url, max_pages, max_concurrent, use_ai): | |
| # result = await run_ai_visibility_analysis( | |
| # base_url=url, | |
| # max_pages=int(max_pages), | |
| # max_concurrent=int(max_concurrent), | |
| # use_ai=use_ai | |
| # ) | |
| # return format_ai_result(result) | |
| # # ---- Gradio Interface ---- | |
| # with gr.Blocks(title="SEO & AI Visibility Analyzer") as demo: | |
| # gr.Markdown("# π SEO & AI Visibility Analysis Tool") | |
| # gr.Markdown("Enter a website URL to analyze its SEO health and AI search readiness.") | |
| # with gr.Row(): | |
| # with gr.Column(scale=2): | |
| # url_input = gr.Textbox(label="Website URL", placeholder="https://example.com", value="https://example.com") | |
| # with gr.Column(scale=1): | |
| # max_pages_input = gr.Number(label="Max Pages", value=3, minimum=1, maximum=20, step=1) | |
| # with gr.Column(scale=1): | |
| # max_concurrent_input = gr.Number(label="Concurrent Browsers", value=1, minimum=1, maximum=5, step=1) | |
| # with gr.Column(scale=1): | |
| # use_ai_check = gr.Checkbox(label="Enable AI Suggestions", value=True) | |
| # with gr.Row(): | |
| # seo_btn = gr.Button("π Analyze SEO", variant="primary") | |
| # ai_btn = gr.Button("π€ Analyze AI Visibility", variant="secondary") | |
| # output = gr.Markdown(label="Results") | |
| # seo_btn.click( | |
| # fn=analyze_seo_async, | |
| # inputs=[url_input, max_pages_input, max_concurrent_input, use_ai_check], | |
| # outputs=output | |
| # ) | |
| # ai_btn.click( | |
| # fn=analyze_ai_async, | |
| # inputs=[url_input, max_pages_input, max_concurrent_input, use_ai_check], | |
| # outputs=output | |
| # ) | |
| # if __name__ == "__main__": | |
| # demo.launch(server_name="0.0.0.0", server_port=7860) |