import gradio as gr import os import tempfile import cv2 import numpy as np import urllib.parse from screencoder.main import generate_html_for_demo from PIL import Image import shutil import html import base64 from bs4 import BeautifulSoup from pathlib import Path # Predefined examples # HF Spaces: Update paths to be relative to the app's root examples_data = [ [ "screencoder/data/input/test1.png", "", "", "", "", "screencoder/data/input/test1.png" ], [ "screencoder/data/input/test2.png", "", "", "", "", "screencoder/data/input/test2.png" ], [ "screencoder/data/input/test3.png", "", "", "", "", "screencoder/data/input/test3.png" ], ] TAILWIND_SCRIPT = "" def image_to_data_url(image_path): """Convert an image file to a data URL for embedding in HTML.""" try: with open(image_path, 'rb') as img_file: img_data = img_file.read() # Detect image type from file extension ext = os.path.splitext(image_path)[1].lower() mime_type = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' }.get(ext, 'image/png') encoded = base64.b64encode(img_data).decode('utf-8') return f'data:{mime_type};base64,{encoded}' except Exception as e: print(f"Error converting image to data URL: {e}") return None def render_preview(code: str, width: int, height: int, scale: float) -> str: """ Preview renderer with both width and height control for the inner canvas. """ safe_code = html.escape(code).replace("'", "'") iframe_html = f"""
""" return iframe_html def process_and_generate(image_np, image_path_from_state, sidebar_prompt, header_prompt, navigation_prompt, main_content_prompt): """ Main processing pipeline: takes an image, generates code, creates a downloadable package, and returns the initial preview and code outputs. """ final_image_path = "" is_temp_file = False if image_path_from_state: final_image_path = image_path_from_state elif image_np is not None: is_temp_file = True with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: cv2.imwrite(tmp.name, cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)) final_image_path = tmp.name else: return "No image provided.", "Please upload or select an image.", gr.update(visible=False), None instructions = { "sidebar": sidebar_prompt, "header": header_prompt, "navigation": navigation_prompt, "main content": main_content_prompt } html_content, run_id = generate_html_for_demo(final_image_path, instructions) if not run_id: # Handle potential errors from the generator return "Generation failed.", f"Error: {html_content}", gr.update(visible=False), None # Rewrite image paths to be absolute for Gradio serving # HF Spaces: Use Path objects for robust path handling base_dir = Path(__file__).parent.resolve() soup = BeautifulSoup(html_content, 'html.parser') print(f"Processing HTML for run_id: {run_id}") for img in soup.find_all('img'): if img.get('src') and not img['src'].startswith(('http', 'data:')): original_src = img['src'] print(f"Processing image: {original_src}") # In HF Spaces, paths can be tricky. We'll rely on the fact # that the image replacer creates a predictable structure. img_path = base_dir / 'screencoder' / 'data' / 'output' / run_id / original_src if img_path.exists(): print(f"Found image at: {img_path}") # Convert to base64 data URL for better iframe compatibility data_url = image_to_data_url(str(img_path)) if data_url: print(f"Converted to data URL: {original_src}") img['src'] = data_url else: # Fallback to Gradio file path (might not work in all Spaces configs) img['src'] = f'/file={str(img_path)}' else: print(f"Image not found at expected path: {img_path}") # Keep original path as fallback img['src'] = original_src html_content = str(soup) output_dir = base_dir / 'screencoder' / 'data' / 'output' / run_id packages_dir = base_dir / 'screencoder' / 'data' / 'packages' packages_dir.mkdir(exist_ok=True) shutil.make_archive(str(packages_dir / run_id), 'zip', str(output_dir)) package_path = packages_dir / f'{run_id}.zip' package_url = f'/file={str(package_path)}' if is_temp_file: os.unlink(final_image_path) initial_preview = render_preview(html_content, 1280, 600, 0.7) return initial_preview, html_content, gr.update(value=package_url, visible=True) with gr.Blocks(head=TAILWIND_SCRIPT, theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky")) as demo: gr.Markdown("# ScreenCoder: Screenshot to Code") with gr.Row(): with gr.Column(scale=1): gr.Markdown("### Step 1: Provide an Image") active_image = gr.Image(type="numpy", height=400, value=examples_data[0][0]) upload_button = gr.UploadButton("Click to Upload", file_types=["image"], variant="primary") gr.Markdown("### Step 2: Write Prompts (Optional)") with gr.Accordion("Component-specific Prompts", open=False): sidebar_prompt = gr.Textbox(label="Sidebar", placeholder="Instructions for the sidebar...") header_prompt = gr.Textbox(label="Header", placeholder="Instructions for the header...") navigation_prompt = gr.Textbox(label="Navigation", placeholder="Instructions for the navigation...") main_content_prompt = gr.Textbox(label="Main Content", placeholder="Instructions for the main content...") generate_btn = gr.Button("Generate HTML", variant="primary") with gr.Column(scale=2): with gr.Tabs(): with gr.TabItem("Preview"): with gr.Row(): scale_slider = gr.Slider(0.2, 1.5, value=0.7, step=0.05, label="Zoom") width_slider = gr.Slider(400, 1920, value=1280, step=100, label="Canvas Width (px)") height_slider = gr.Slider(300, 1080, value=600, step=50, label="Canvas Height (px)") html_preview = gr.HTML(label="Rendered HTML", show_label=False) with gr.TabItem("Code"): html_code_output = gr.Code(label="Generated HTML", language="html") download_button = gr.Button("⬇️ Download Package", visible=False, variant="secondary") gr.Examples( examples=examples_data, fn=lambda *args: args[0], inputs=[gr.State(examples_data[0][0])], outputs=[active_image], cache_examples=False, ) active_image_path_state = gr.State(examples_data[0][5]) def handle_example_click(img_path): return img_path, img_path demo.load( lambda: (examples_data[0][0], examples_data[0][5]), None, [active_image, active_image_path_state] ) def handle_upload(uploaded_image_np): return uploaded_image_np, None, gr.update(visible=False) upload_button.upload(handle_upload, upload_button, [active_image, active_image_path_state, download_button]) generate_btn.click( process_and_generate, [active_image, active_image_path_state, sidebar_prompt, header_prompt, navigation_prompt, main_content_prompt], [html_preview, html_code_output, download_button], show_progress="full" ) preview_controls = [scale_slider, width_slider, height_slider] for control in preview_controls: control.change( render_preview, [html_code_output, width_slider, height_slider, scale_slider], html_preview, show_progress=True ) download_button.click(None, download_button, None, js= \ "(url) => { const link = document.createElement('a'); link.href = url; link.download = ''; document.body.appendChild(link); link.click(); document.body.removeChild(link); }") base_dir = Path(__file__).parent.resolve() allowed_paths = [ str(base_dir), str(base_dir / 'screencoder' / 'data' / 'output'), str(base_dir / 'screencoder' / 'data' / 'packages') ] # Add all example file paths to allowed_paths to ensure they are accessible for example in examples_data: # HF Spaces: Ensure the path is absolute by joining with base_dir # The example path is relative, so we join it with the base_dir to make it absolute. example_abs_path = (base_dir / example[0]).resolve() example_dir = example_abs_path.parent if str(example_dir) not in allowed_paths: allowed_paths.append(str(example_dir)) print("Allowed paths for file serving:") for path in allowed_paths: print(f" - {path}") if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=7860, allowed_paths=allowed_paths )