"""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)