All articles
Build Guides

Build a Screenshot-to-Code Agent with Llama 3.2 Vision (Free Tier)

FDE Coach EditorialAugust 8, 20269 min read

What We're Building

We're shipping a zero-cost CLI tool that ingests a UI mockup screenshot and spits out a single, self-contained HTML file styled with Tailwind CSS. No API keys. No credit card. No flaky multi-agent chains. Just a sharp prompt, a base64-encoded image, and OpenRouter's free Llama 3.2 11B Vision model.

Feature list:

  • Accepts any PNG/JPG UI mockup via command-line argument
  • Calls OpenRouter's free Llama 3.2 Vision endpoint (no auth required on free models)
  • Returns a complete HTML document with Tailwind CDN, responsive utility classes, and placeholder content
  • Zero runtime dependencies beyond Python stdlib and requests
  • Single-file output ready to open in a browser

This isn't a toy. Engineers at forward-deployed teams use this exact pattern to close the gap between design artifacts and working prototypes in minutes. If you're preparing for roles where shipping velocity matters, this is the kind of muscle memory you build. The FDE Interview Loop rewards builders who can demonstrate exactly this—working code, not slide decks.

Architecture: The Vision-to-Code Pipeline

Before we write a single line, let's map the data flow. The system has five logical stages: image ingestion, encoding, prompt construction, LLM inference, and response extraction.

The critical decision: we use OpenRouter's free tier, which routes to Llama 3.2 11B Vision at zero cost. No self-hosting, no GPU rental. The trade-off is rate limits and occasional cold starts, but for a prototyping tool, it's a no-brainer.

Prerequisites (All Free)

You need exactly three things:

ComponentPurposeLink
Python 3.10+Runtimehttps://www.python.org/downloads/
requests libraryHTTP clientpip install requests
OpenRouter free tierLLM accesshttps://openrouter.ai/ (no key required for free models)

That's it. No Docker, no vector database, no cloud account. OpenRouter's free models don't even require an API key—you can hit the endpoint anonymously with rate limits. For production use you'd add a key for higher limits, but we're staying strictly zero-cost.

Step 1: Project Setup and Dependencies

Create a single directory and a virtual environment:

mkdir screenshot-to-code
cd screenshot-to-code
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install requests

Create main.py—this will hold everything. We're deliberately avoiding framework sprawl. One file, one responsibility.

#!/usr/bin/env python3
"""Screenshot-to-Code Agent using OpenRouter's free Llama 3.2 Vision."""
import sys
import base64
import json
import argparse
from pathlib import Path

import requests

Step 2: The Prompt Strategy That Actually Works

Vision models are sensitive to prompt structure. Vague instructions produce vague output. We need surgical precision. Here's the prompt template that consistently delivers clean, usable HTML:

SYSTEM_PROMPT = """You are a senior frontend engineer. When given a UI mockup screenshot, you produce a complete, single-file HTML document using Tailwind CSS via CDN.

Rules:
- Use only Tailwind utility classes (no custom CSS)
- Include the Tailwind CDN in <head>: <script src="https://cdn.tailwindcss.com"></script>
- Make the layout responsive (mobile-first)
- Use semantic HTML elements
- Add realistic placeholder text (lorem ipsum for body, real-sounding labels for buttons/inputs)
- Output ONLY the raw HTML. No markdown fences, no explanations, no "Here is the code"
- The first character of your response must be `<`
- The last character must be `>`
"""

Why this works: we constrain the output format aggressively. Vision models love to wrap code in markdown fences or add commentary. By demanding the response start with < and end with >, we make extraction trivial. The "no explanations" rule eliminates the model's tendency to narrate its work.

Step 3: Encoding and Sending the Screenshot

OpenRouter's chat completions endpoint accepts images as base64-encoded data URIs in the standard OpenAI vision format. Here's the encoding and API call:

def encode_image(image_path: str) -> str:
    """Read an image file and return a base64 data URI."""
    path = Path(image_path)
    if not path.exists():
        raise FileNotFoundError(f"Image not found: {image_path}")
    
    suffix = path.suffix.lower()
    mime_type = "image/png" if suffix == ".png" else "image/jpeg"
    
    with open(path, "rb") as f:
        b64 = base64.b64encode(f.read()).decode("utf-8")
    
    return f"data:{mime_type};base64,{b64}"


def call_vision_api(image_data_uri: str, user_prompt: str) -> str:
    """Send the image and prompt to OpenRouter's free Llama 3.2 Vision."""
    url = "https://openrouter.ai/api/v1/chat/completions"
    
    headers = {
        "Content-Type": "application/json",
        # No Authorization header needed for free models
    }
    
    payload = {
        "model": "meta-llama/llama-3.2-11b-vision-instruct:free",
        "messages": [
            {
                "role": "system",
                "content": SYSTEM_PROMPT
            },
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": user_prompt
                    },
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": image_data_uri
                        }
                    }
                ]
            }
        ],
        "max_tokens": 4096,
        "temperature": 0.1  # Low temp for deterministic code generation
    }
    
    response = requests.post(url, headers=headers, json=payload, timeout=120)
    response.raise_for_status()
    
    return response.json()["choices"][0]["message"]["content"]

Key details:

  • temperature: 0.1 keeps the output deterministic. UI code isn't creative writing.
  • 120-second timeout accounts for cold starts on free-tier inference.
  • No API key header—OpenRouter allows anonymous access for free models.

Step 4: Parsing the LLM Response

Despite our strict prompt, models occasionally wrap output in markdown fences. We need a robust extraction function:

def extract_html(raw_response: str) -> str:
    """Extract clean HTML from the LLM response, handling markdown fences."""
    text = raw_response.strip()
    
    # Strip markdown code fences if present
    if text.startswith("```"):
        lines = text.split("\n")
        # Remove opening fence (might include language tag)
        if lines[0].startswith("```"):
            lines = lines[1:]
        # Remove closing fence
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        text = "\n".join(lines).strip()
    
    return text

Step 5: The Complete CLI Script

Here's the full main.py wired together with argument parsing and file output:

def main():
    parser = argparse.ArgumentParser(
        description="Convert a UI mockup screenshot to HTML/Tailwind code."
    )
    parser.add_argument(
        "image",
        help="Path to the screenshot (PNG or JPG)"
    )
    parser.add_argument(
        "-o", "--output",
        default="output.html",
        help="Output HTML file path (default: output.html)"
    )
    parser.add_argument(
        "-p", "--prompt",
        default="Generate a complete HTML/Tailwind implementation of this UI mockup.",
        help="Custom user prompt to append to system instructions"
    )
    
    args = parser.parse_args()
    
    print(f"📸 Encoding image: {args.image}")
    image_uri = encode_image(args.image)
    
    print(f"🤖 Calling Llama 3.2 Vision (free tier)...")
    raw_html = call_vision_api(image_uri, args.prompt)
    
    print(f"🔧 Extracting clean HTML...")
    clean_html = extract_html(raw_html)
    
    output_path = Path(args.output)
    output_path.write_text(clean_html, encoding="utf-8")
    
    print(f"✅ Done! Open {output_path} in your browser.")
    print(f"   File size: {output_path.stat().st_size:,} bytes")


if __name__ == "__main__":
    main()

Running the Agent

Save a UI mockup as mockup.png (or grab one from Dribbble, Figma, or a quick hand-drawn sketch) and run:

python main.py mockup.png

Output:

📸 Encoding image: mockup.png
🤖 Calling Llama 3.2 Vision (free tier)...
🔧 Extracting clean HTML...
✅ Done! Open output.html in your browser.
   File size: 3,421 bytes

Open output.html in Chrome or Firefox. You'll see a Tailwind-styled page matching the mockup's layout structure. It won't be pixel-perfect—vision models hallucinate spacing and colors—but it's a solid starting point that eliminates blank-file syndrome.

For a custom user prompt:

python main.py dashboard.png -p "Focus on the sidebar navigation and data table. Use slate color palette."

Sensible Extensions

Once the core loop works, here's where to take it:

1. Batch Processing Wrap the script in a loop to process an entire directory of mockups:

for f in mockups/*.png; do python main.py "$f" -o "output/$(basename $f .png).html"; done

2. Iterative Refinement Feed the generated HTML back as context with a refinement prompt. This is effectively a two-pass approach: first pass generates structure, second pass tightens spacing and colors. You could build this into the same script with a --refine flag.

3. Component Library Extraction If you consistently generate similar components (navbars, cards, modals), extract them into a prompt library. This mirrors how FDEs build reusable artifacts—the highest-leverage skills in the AI era are exactly this: prompt engineering plus rapid data prep.

4. Direct Browser Preview Add a --open flag that auto-launches the default browser:

import webbrowser
# After writing output:
if args.open:
    webbrowser.open(f"file://{output_path.absolute()}")

Common Pitfalls

"Model not found" errors. Free models on OpenRouter occasionally rotate. Check https://openrouter.ai/models?q=free for the current free vision model ID. Update the model field in the payload accordingly.

Empty or truncated responses. Free tier has token limits. If your mockup is complex, the model may hit the 4096 token ceiling. Split complex pages into component-level screenshots and process individually.

Garbled layouts. Vision models struggle with spatial reasoning—a known limitation we covered in LLMs Can't Jump: Why Spatial Reasoning Remains a Hard Ceiling. Treat the output as a first draft. Expect to tweak padding, grid columns, and alignment manually.

Rate limiting. Anonymous access is rate-limited. If you hit 429 errors, add a 5-second sleep between requests or sign up for a free OpenRouter key (still free, just higher limits).

Tailwind CDN in production. The CDN script is great for prototyping but ships the full Tailwind compiler to the browser. For anything beyond internal tools, run the Tailwind CLI to generate a purged CSS file.

FAQ

Q: Do I really not need an API key? A: Correct. OpenRouter serves a rotating set of free models without authentication. You can verify this by curling the endpoint directly. If you want higher rate limits or access to paid models, you'll need a key, but our tool works without one.

Q: How good is Llama 3.2 Vision at this task? A: Surprisingly competent for layout structure, surprisingly bad at precise measurements. It'll get the general arrangement right—header, sidebar, card grid—but expect to adjust specific pixel values and color hex codes.

Q: Can I use this for production code? A: Not directly. The output is a prototype. Use it to accelerate the first draft, then refactor. The generated HTML has no accessibility attributes, no JavaScript interactivity, and uses CDN Tailwind. It's a starting line, not a finish line.

Q: What image formats work? A: PNG and JPEG. The script auto-detects MIME type from the file extension. For other formats, add the appropriate MIME mapping in encode_image().

Q: How does this compare to paid screenshot-to-code tools? A: Commercial tools (v0, Claude Artifacts, etc.) produce higher-fidelity output because they use larger models and iterative refinement loops. But they cost money and lock you into platforms. This tool is free, local, and gives you full control over the prompt and output. For an FDE shipping customer prototypes, that control is worth more than pixel-perfect CSS.

Q: What if I want to generate React or Vue components instead? A: Modify the system prompt to request JSX or SFC format. The vision model doesn't care about the target framework—it just follows instructions. The same architecture works for any text output format.

Q: Why not use a local model with Ollama? A: You can. Swap the API call for an Ollama chat endpoint. The trade-off: local inference requires a GPU with enough VRAM for an 11B-parameter vision model. OpenRouter's free tier offloads that compute. Choose based on your hardware and latency requirements.

#vision-llm#ui-generation#tailwind

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now