sayakpaul's picture
sayakpaul HF Staff
Update app.py
582305c verified
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()