Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import requests | |
| import io | |
| import tempfile | |
| from svglib.svglib import svg2rlg | |
| from reportlab.graphics import renderPM | |
| # Language colors mapping (common languages) | |
| LANG_COLORS = { | |
| "Python": "#3572A5", "JavaScript": "#f1e05a", "TypeScript": "#3178c6", | |
| "Java": "#b07219", "C++": "#f34b7d", "C": "#555555", "C#": "#178600", | |
| "Go": "#00ADD8", "Rust": "#dea584", "Ruby": "#701516", "PHP": "#4F5D95", | |
| "Swift": "#F05138", "Kotlin": "#A97BFF", "Scala": "#c22d40", | |
| "HTML": "#e34c26", "CSS": "#563d7c", "Shell": "#89e051", "Lua": "#000080", | |
| "R": "#198CE7", "Dart": "#00B4AB", "Vue": "#41b883", "Jupyter Notebook": "#DA5B0B", | |
| } | |
| def get_lang_color(lang): | |
| return LANG_COLORS.get(lang, "#586069") | |
| def fetch_repo_data(repo_id: str): | |
| """Fetch repository data from GitHub API""" | |
| repo_id = repo_id.strip() | |
| if repo_id.startswith("https://github.com/"): | |
| repo_id = repo_id.replace("https://github.com/", "") | |
| repo_id = repo_id.rstrip("/") | |
| if "/" not in repo_id: | |
| return None, "Invalid repo ID. Use format: owner/repo" | |
| url = f"https://api.github.com/repos/{repo_id}" | |
| try: | |
| resp = requests.get(url, timeout=10) | |
| if resp.status_code == 404: | |
| return None, f"Repository '{repo_id}' not found" | |
| if resp.status_code == 403: | |
| return None, "GitHub API rate limit exceeded. Try again later." | |
| resp.raise_for_status() | |
| return resp.json(), None | |
| except Exception as e: | |
| return None, f"Error fetching data: {str(e)}" | |
| def format_number(n): | |
| if n >= 1000: | |
| return f"{n/1000:.1f}k" | |
| return str(n) | |
| def generate_card_html(repo_id: str): | |
| """Generate a beautiful HTML card for the repository""" | |
| data, error = fetch_repo_data(repo_id) | |
| if error: | |
| return f'<div style="padding:20px;color:#d73a49;background:#ffeef0;border-radius:8px;font-family:system-ui;">{error}</div>' | |
| name = data.get("name", "") | |
| full_name = data.get("full_name", "") | |
| description = data.get("description") or "No description provided" | |
| language = data.get("language") or "" | |
| stars = data.get("stargazers_count", 0) | |
| forks = data.get("forks_count", 0) | |
| html_url = data.get("html_url", "#") | |
| owner = data.get("owner", {}) | |
| avatar = owner.get("avatar_url", "") | |
| lang_color = get_lang_color(language) | |
| # Build language section separately | |
| lang_section = "" | |
| if language: | |
| lang_section = f'''<div style="display: flex; align-items: center; gap: 6px;"> | |
| <span style="width: 12px; height: 12px; border-radius: 50%; background-color: {lang_color}; display: inline-block;"></span> | |
| <span style="font-size: 13px; color: #57606a;">{language}</span> | |
| </div>''' | |
| html = f''' | |
| <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; max-width: 420px;"> | |
| <a href="{html_url}" target="_blank" style="text-decoration: none; color: inherit; display: block;"> | |
| <div style="border: 1px solid #d0d7de; border-radius: 12px; padding: 20px; background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%); box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: all 0.2s ease;"> | |
| <!-- Header with icon and repo name --> | |
| <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;"> | |
| <div style="width: 20px; height: 20px; flex-shrink: 0;"> | |
| <svg viewBox="0 0 16 16" width="20" height="20" fill="#656d76"> | |
| <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"></path> | |
| </svg> | |
| </div> | |
| <span style="font-size: 16px; font-weight: 600; color: #0969da; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{full_name}</span> | |
| </div> | |
| <!-- Description --> | |
| <p style="font-size: 14px; color: #57606a; margin: 0 0 16px 0; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-align: left;">{description}</p> | |
| <!-- Stats row --> | |
| <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;"> | |
| <!-- Language --> | |
| {lang_section} | |
| <!-- Stars --> | |
| <div style="display: flex; align-items: center; gap: 4px;"> | |
| <svg viewBox="0 0 16 16" width="16" height="16" fill="#656d76"> | |
| <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path> | |
| </svg> | |
| <span style="font-size: 13px; color: #57606a;">{format_number(stars)}</span> | |
| </div> | |
| <!-- Forks --> | |
| <div style="display: flex; align-items: center; gap: 4px;"> | |
| <svg viewBox="0 0 16 16" width="16" height="16" fill="#656d76"> | |
| <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path> | |
| </svg> | |
| <span style="font-size: 13px; color: #57606a;">{format_number(forks)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </a> | |
| </div> | |
| ''' | |
| return html | |
| def generate_card_svg(repo_id: str): | |
| """Generate an SVG card for embedding""" | |
| data, error = fetch_repo_data(repo_id) | |
| if error: | |
| return f'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120"><text x="20" y="60" fill="#d73a49">{error}</text></svg>' | |
| name = data.get("name", "") | |
| full_name = data.get("full_name", "") | |
| desc = data.get("description") or "No description" | |
| if len(desc) > 60: | |
| desc = desc[:57] + "..." | |
| language = data.get("language") or "" | |
| stars = format_number(data.get("stargazers_count", 0)) | |
| forks = format_number(data.get("forks_count", 0)) | |
| lang_color = get_lang_color(language) | |
| # Build language section separately | |
| lang_svg = "" | |
| if language: | |
| lang_svg = f'<circle cx="28" cy="110" r="6" fill="{lang_color}"/><text x="42" y="114" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif" font-size="12" fill="#57606a">{language}</text>' | |
| star_x = "160" if language else "28" | |
| star_text_x = "178" if language else "46" | |
| fork_x = "220" if language else "88" | |
| fork_text_x = "248" if language else "116" | |
| svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="400" height="140" viewBox="0 0 400 140"> | |
| <defs> | |
| <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#ffffff"/> | |
| <stop offset="100%" style="stop-color:#f6f8fa"/> | |
| </linearGradient> | |
| </defs> | |
| <rect width="400" height="140" rx="12" fill="url(#bg)" stroke="#d0d7de" stroke-width="1"/> | |
| <!-- Repo icon --> | |
| <path d="M22 22.5A2.5 2.5 0 0 1 24.5 20h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 22 31.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 24.5 29h8ZM25 32.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L25.4 35.7a.25.25 0 0 1-.4-.2Z" fill="#656d76"/> | |
| <!-- Repo name --> | |
| <text x="48" y="37" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif" font-size="15" font-weight="600" fill="#0969da">{full_name}</text> | |
| <!-- Description --> | |
| <text x="20" y="70" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif" font-size="12" fill="#57606a" text-anchor="start">{desc}</text> | |
| <!-- Language --> | |
| {lang_svg} | |
| <!-- Star icon --> | |
| <svg x="{star_x}" y="100" width="16" height="16" viewBox="0 0 16 16"> | |
| <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" fill="#656d76"/> | |
| </svg> | |
| <text x="{star_text_x}" y="114" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif" font-size="12" fill="#57606a">{stars}</text> | |
| <!-- Fork icon --> | |
| <svg x="{fork_x}" y="100" width="16" height="16" viewBox="0 0 16 16"> | |
| <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z" fill="#656d76"/> | |
| </svg> | |
| <text x="{fork_text_x}" y="114" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif" font-size="12" fill="#57606a">{forks}</text> | |
| </svg>''' | |
| return svg | |
| def generate_card_png(repo_id: str): | |
| """Generate a PNG image from the SVG card""" | |
| svg_content = generate_card_svg(repo_id) | |
| # Check if it's an error SVG | |
| if "not found" in svg_content or "Error" in svg_content or "Invalid" in svg_content: | |
| return None | |
| try: | |
| # Write SVG to a temporary file | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.svg', delete=False) as f: | |
| f.write(svg_content) | |
| temp_svg_path = f.name | |
| # Convert SVG to PNG | |
| drawing = svg2rlg(temp_svg_path) | |
| # Scale up for better quality | |
| scale = 2 | |
| drawing.width *= scale | |
| drawing.height *= scale | |
| drawing.scale(scale, scale) | |
| # Create PNG in memory | |
| png_data = io.BytesIO() | |
| renderPM.drawToFile(drawing, png_data, fmt="PNG", bg=0xFFFFFF) | |
| png_data.seek(0) | |
| # Save to a temporary file for Gradio | |
| with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as f: | |
| f.write(png_data.read()) | |
| return f.name | |
| except Exception as e: | |
| print(f"Error generating PNG: {e}") | |
| return None | |
| # Create Gradio interface | |
| with gr.Blocks( | |
| title="GitHub Repo Card Generator", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .container { max-width: 800px; margin: auto; } | |
| .gr-button-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; } | |
| """ | |
| ) as demo: | |
| gr.Markdown( | |
| """ | |
| # 🎴 GitHub Repository Card Generator | |
| Generate beautiful, embeddable cards for any public GitHub repository. | |
| Enter a repository ID (e.g., `facebook/react` or `huggingface/transformers`) to get started. | |
| """ | |
| ) | |
| with gr.Row(): | |
| repo_input = gr.Textbox( | |
| label="Repository ID", | |
| placeholder="owner/repo (e.g., microsoft/vscode)", | |
| scale=4 | |
| ) | |
| generate_btn = gr.Button("Generate Card", variant="primary", scale=1) | |
| gr.Examples( | |
| examples=[ | |
| "huggingface/transformers", | |
| "facebook/react", | |
| "microsoft/vscode", | |
| "openai/whisper", | |
| "gradio-app/gradio" | |
| ], | |
| inputs=repo_input | |
| ) | |
| with gr.Tabs(): | |
| with gr.Tab("Preview"): | |
| html_output = gr.HTML(label="Card Preview") | |
| with gr.Tab("SVG Code"): | |
| svg_output = gr.Code(label="SVG Code (for embedding)", language="html") | |
| with gr.Tab("Download PNG"): | |
| png_output = gr.File(label="Download PNG", file_types=[".png"]) | |
| # Event handlers | |
| generate_btn.click( | |
| fn=generate_card_html, | |
| inputs=repo_input, | |
| outputs=html_output | |
| ).then( | |
| fn=generate_card_svg, | |
| inputs=repo_input, | |
| outputs=svg_output | |
| ).then( | |
| fn=generate_card_png, | |
| inputs=repo_input, | |
| outputs=png_output | |
| ) | |
| repo_input.submit( | |
| fn=generate_card_html, | |
| inputs=repo_input, | |
| outputs=html_output | |
| ).then( | |
| fn=generate_card_svg, | |
| inputs=repo_input, | |
| outputs=svg_output | |
| ).then( | |
| fn=generate_card_png, | |
| inputs=repo_input, | |
| outputs=png_output | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |