ditherforge-api / app.py
wisamWR's picture
fix: add @spaces.GPU decorator for ZeroGPU compatibility
8c2076c
Raw History Blame Contribute Delete
7.14 kB
"""DitherForge API — Gradio app with text, avatar, and banner endpoints."""
from __future__ import annotations
import gradio as gr
import spaces
import requests
from io import BytesIO
from PIL import Image
import numpy as np
from effects import binary_effect, ascii_effect, braille_effect, image_to_gray
from svg_builder import text_to_svg
STYLES = {"binary": binary_effect, "ascii": ascii_effect, "braille": braille_effect}
ANIMATIONS = ["none", "reveal", "scanline", "flicker", "matrix"]
def text_endpoint(
text: str,
style: str = "ascii",
color: str = "#00ff41",
bg_color: str = "#0d1117",
animation: str = "none",
duration: float = 2.0,
font_size: int = 14,
) -> tuple[str, str]:
"""Convert text string to styled SVG. Returns (html_preview, svg_source)."""
effect_fn = STYLES.get(style, ascii_effect)
lines = text.split("\n") if "\n" in text else [text]
max_len = max(len(line) for line in lines)
h_pixels = len(lines) * 16
w_pixels = max_len * 10
gray = np.zeros((h_pixels, w_pixels), dtype=np.uint8)
for row_idx, line in enumerate(lines):
for col_idx, char in enumerate(line):
brightness = (ord(char) * 37) % 256
y0, y1 = row_idx * 16, (row_idx + 1) * 16
x0, x1 = col_idx * 10, (col_idx + 1) * 10
if y1 <= h_pixels and x1 <= w_pixels:
gray[y0:y1, x0:x1] = brightness
art_text = effect_fn(gray, cols=max_len)
svg = text_to_svg(art_text, font_size, color, bg_color, animation, duration)
return svg, svg
def avatar_endpoint(
image: Image.Image | None = None,
image_url: str = "",
style: str = "ascii",
cols: int = 60,
color: str = "#00ff41",
bg_color: str = "#0d1117",
animation: str = "none",
duration: float = 2.0,
) -> tuple[str, str]:
"""Convert uploaded image or image URL to ASCII art SVG."""
if image is None and image_url:
resp = requests.get(image_url, timeout=10)
resp.raise_for_status()
image = Image.open(BytesIO(resp.content))
if image is None:
svg = text_to_svg("No image provided", 14, color, bg_color)
return svg, svg
gray = image_to_gray(image)
effect_fn = STYLES.get(style, ascii_effect)
art_text = effect_fn(gray, cols=cols)
svg = text_to_svg(art_text, 14, color, bg_color, animation, duration)
return svg, svg
@spaces.GPU(duration=5)
def banner_endpoint(
text: str = "DitherForge",
style: str = "binary",
cols: int = 40,
color: str = "#00ff41",
bg_color: str = "#0d1117",
animation: str = "reveal",
duration: float = 2.0,
) -> tuple[str, str]:
"""Generate a banner SVG from parameters."""
from PIL import ImageDraw, ImageFont
font_size = 40
char_w = font_size * 0.6
img_w = max(int(len(text) * char_w) + 20, 200)
img_h = font_size + 20
img = Image.new("L", (img_w, img_h), 255)
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", font_size
)
except (OSError, IOError):
font = ImageFont.load_default()
draw.text((10, 10), text, fill=0, font=font)
gray = np.array(img, dtype=np.uint8)
effect_fn = STYLES.get(style, binary_effect)
art_text = effect_fn(gray, cols=cols)
svg = text_to_svg(art_text, 12, color, bg_color, animation, duration)
return svg, svg
# --- Gradio UI ---
with gr.Blocks(title="DitherForge API") as demo:
gr.Markdown(
"# DitherForge API\n"
"Generate ASCII/binary/braille art as SVG for GitHub README embeds."
)
with gr.Tab("Text to SVG"):
with gr.Row():
with gr.Column():
txt_input = gr.Textbox(label="Text", value="Hello World")
txt_style = gr.Dropdown(
list(STYLES.keys()), value="ascii", label="Style"
)
txt_color = gr.ColorPicker(label="Text Color", value="#00ff41")
txt_bg = gr.ColorPicker(label="Background", value="#0d1117")
txt_anim = gr.Dropdown(ANIMATIONS, value="none", label="Animation")
txt_dur = gr.Slider(
0.5, 5.0, value=2.0, step=0.1, label="Duration (s)"
)
txt_btn = gr.Button("Generate", variant="primary")
with gr.Column():
txt_out = gr.HTML(label="Preview")
txt_svg = gr.Code(label="SVG Source", language="html")
txt_btn.click(
text_endpoint,
[txt_input, txt_style, txt_color, txt_bg, txt_anim, txt_dur],
[txt_out, txt_svg],
api_name="text",
)
with gr.Tab("Image to ASCII SVG"):
with gr.Row():
with gr.Column():
img_input = gr.Image(type="pil", label="Upload Image")
img_url = gr.Textbox(
label="Or paste image URL", placeholder="https://..."
)
img_style = gr.Dropdown(
list(STYLES.keys()), value="ascii", label="Style"
)
img_cols = gr.Slider(20, 120, value=60, step=1, label="Columns")
img_color = gr.ColorPicker(label="Text Color", value="#00ff41")
img_bg = gr.ColorPicker(label="Background", value="#0d1117")
img_anim = gr.Dropdown(ANIMATIONS, value="none", label="Animation")
img_btn = gr.Button("Generate", variant="primary")
with gr.Column():
img_out = gr.HTML(label="Preview")
img_svg = gr.Code(label="SVG Source", language="html")
img_btn.click(
avatar_endpoint,
[img_input, img_url, img_style, img_cols, img_color, img_bg, img_anim],
[img_out, img_svg],
api_name="avatar",
)
with gr.Tab("Banner"):
with gr.Row():
with gr.Column():
ban_text = gr.Textbox(label="Banner Text", value="DitherForge")
ban_style = gr.Dropdown(
list(STYLES.keys()), value="binary", label="Style"
)
ban_cols = gr.Slider(20, 100, value=40, step=1, label="Columns")
ban_color = gr.ColorPicker(label="Color", value="#00ff41")
ban_bg = gr.ColorPicker(label="Background", value="#0d1117")
ban_anim = gr.Dropdown(
ANIMATIONS, value="reveal", label="Animation"
)
ban_dur = gr.Slider(
0.5, 5.0, value=2.0, step=0.1, label="Duration (s)"
)
ban_btn = gr.Button("Generate", variant="primary")
with gr.Column():
ban_out = gr.HTML(label="Preview")
ban_svg = gr.Code(label="SVG Source", language="html")
ban_btn.click(
banner_endpoint,
[ban_text, ban_style, ban_cols, ban_color, ban_bg, ban_anim, ban_dur],
[ban_out, ban_svg],
api_name="banner",
)
if __name__ == "__main__":
demo.launch(ssr_mode=False)