File size: 13,038 Bytes
be8b6de
a457c97
be8b6de
 
 
 
a98acba
360569f
 
6935192
3b38290
7d2610b
be8b6de
6741cd4
6c870fa
534e13f
6741cd4
be8b6de
 
 
6741cd4
be8b6de
 
 
df66a58
c485c19
df66a58
be8b6de
 
360569f
be8b6de
534e13f
1a7b6de
 
 
be8b6de
6741cd4
d1685d3
4084cf0
be8b6de
 
a98acba
 
be8b6de
3b38290
 
 
 
4084cf0
d1685d3
be8b6de
7d2610b
534e13f
7d2610b
 
 
 
 
 
 
be8b6de
4084cf0
7d2610b
6741cd4
a98acba
360569f
4084cf0
360569f
fcfb056
 
 
 
 
 
 
37adb99
fcfb056
7d2610b
fcfb056
4084cf0
fcfb056
 
 
37adb99
fcfb056
 
 
 
 
 
7d2610b
534e13f
8871397
 
534e13f
fcfb056
360569f
37adb99
360569f
 
 
37adb99
360569f
 
 
3452e4e
360569f
 
8871397
7d2610b
360569f
3452e4e
7d2610b
 
 
 
7e3ef54
4084cf0
be8b6de
6741cd4
4084cf0
be8b6de
a98acba
be8b6de
 
 
7e3ef54
 
 
a622f66
fcfb056
4084cf0
fcfb056
360569f
 
a622f66
 
 
 
360569f
 
fcfb056
4084cf0
7e3ef54
 
 
 
 
 
a622f66
be8b6de
4084cf0
df66a58
a98acba
4084cf0
be8b6de
7d2610b
be8b6de
7d2610b
3452e4e
534e13f
3452e4e
534e13f
 
 
 
3452e4e
8871397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3452e4e
 
 
 
64bf601
3452e4e
534e13f
3452e4e
80d93f2
3452e4e
8871397
 
3452e4e
 
64bf601
80d93f2
3452e4e
534e13f
3452e4e
7e3ef54
4084cf0
8871397
 
 
a622f66
534e13f
a622f66
3452e4e
80d93f2
4084cf0
7d2610b
534e13f
be8b6de
4084cf0
7d2610b
7e3ef54
3452e4e
6741cd4
 
 
 
be8b6de
6741cd4
 
7d2610b
6741cd4
 
 
534e13f
6741cd4
 
 
7e3ef54
 
6741cd4
 
 
 
 
 
 
4084cf0
6741cd4
 
4084cf0
6741cd4
7e3ef54
 
6741cd4
 
 
3452e4e
7d2610b
 
 
6741cd4
3452e4e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import os
from flask import Flask, render_template, request, jsonify
from PIL import Image
import google.generativeai as genai
import base64
import io
import logging
import json
import re
import random
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
from flask_socketio import SocketIO, emit

app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.randint(11111,99999999999999999999999999))
socketio = SocketIO(app, cors_allowed_origins="*")  # For development; restrict in production.

# --- API Key Configuration ---
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
if not GOOGLE_API_KEY:
    raise ValueError("GOOGLE_API_KEY environment variable not set.")

genai.configure(api_key=GOOGLE_API_KEY)

# --- Free-tier Gemini Models ---
AVAILABLE_MODELS = ["gemini-1.5-flash"]
DEFAULT_MODEL = "gemini-1.5-flash"

# --- Optimization Parameters ---
DEFAULT_MAX_HEIGHT = 1000
DEFAULT_IMAGE_FORMAT = "PNG"
DEFAULT_TIMEOUT = 10000

# --- Ensure Playwright uses the same cache path at runtime ---
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/app/.cache/playwright"

# --- Utility Functions ---
def screenshot_from_url(url: str, max_height: int = DEFAULT_MAX_HEIGHT, image_format: str = DEFAULT_IMAGE_FORMAT, timeout: int = DEFAULT_TIMEOUT) -> Image.Image:
    app.logger.info(f"Taking screenshot of {url} with timeout {timeout}ms")
    try:
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            context = browser.new_context()
            page = context.new_page()
            try:
                page.goto(url, timeout=timeout)
                page.wait_for_load_state("networkidle", timeout=timeout)
            except PlaywrightTimeoutError:
                app.logger.warning(f"Timeout waiting for networkidle on {url}. Capturing partial screenshot.")
            screenshot_bytes = page.screenshot(full_page=True, type=image_format.lower(), timeout=timeout)
            browser.close()

            socketio.emit('progress', {'percent': 25, 'message': 'Screenshot taken'})
            image = Image.open(io.BytesIO(screenshot_bytes))
            if image.height > max_height:
                ratio = max_height / image.height
                new_width = int(image.width * ratio)
                image = image.resize((new_width, max_height), Image.LANCZOS)
            app.logger.info("Screenshot captured successfully")
            return image
    except Exception as e:
        app.logger.error(f"Error taking screenshot: {e}")
        socketio.emit('progress', {'percent': 100, 'message': f'Error: {str(e)}'})
        raise Exception(f"Failed to capture screenshot: {str(e)}")

def parse_model_response(response_text: str) -> dict:
    app.logger.info("Parsing model response")
    try:
        # Use regex to find content within START and STOP tokens
        pattern = r"==START_JSON==(.*?)==STOP_JSON=="
        match = re.search(pattern, response_text, re.DOTALL)

        if match:
            json_content = match.group(1).strip()
            files = json.loads(json_content)
            if not isinstance(files, dict) or "files" not in files or not isinstance(files["files"], dict):
                raise ValueError("Invalid JSON structure")
            socketio.emit('progress', {'percent': 90, 'message': 'JSON parsed'})
        else:
            app.logger.warning("No JSON found within START/STOP tokens, attempting custom parsing")
            pattern = r"### (.+?)\n```(?:\w+)?\n(.*?)\n```"
            matches = re.findall(pattern, response_text, re.DOTALL)
            if not matches:
                raise ValueError(f"Could not parse response into files. Response start: {response_text[:200]}")
            files = {
                "files": {
                    filename.strip(): {"content": content.strip()}
                    for filename, content in matches
                }
            }
            socketio.emit('progress', {'percent': 90, 'message': 'Parsed with fallback method'})

        # Keep the original index.html content intact (full HTML with <link> and <script>)
        app.logger.info("Preserving original index.html content")
        return files

    except json.JSONDecodeError:
        app.logger.warning(f"Response is not valid JSON, attempting custom parsing. Response start: {response_text[:200]}")
        pattern = r"### (.+?)\n```(?:\w+)?\n(.*?)\n```"
        matches = re.findall(pattern, response_text, re.DOTALL)
        if not matches:
            raise ValueError(f"Could not parse response into files. Response start: {response_text[:200]}")
        files = {
            "files": {
                filename.strip(): {"content": content.strip()}
                for filename, content in matches
            }
        }
        app.logger.info("Preserving original index.html content")
        socketio.emit('progress', {'percent': 90, 'message': 'Parsed with fallback method'})
        return files
    except ValueError as e:
        app.logger.error(f"ValueError in parsing: {e}")
        socketio.emit('progress', {'percent': 100, 'message': f'Error: {str(e)}'})
        raise

def image_to_html(image: Image.Image, model_name: str) -> tuple[dict, str, str]:
    app.logger.info(f"Converting image to HTML with model {model_name}")
    if image is None:
        raise ValueError("Image is None.")

    buffered = io.BytesIO()
    image.save(buffered, format="PNG")
    img_str = base64.b64encode(buffered.getvalue()).decode()

    prompt = """
    Analyze this webpage screenshot and generate a complete and fully functional code package to recreate it **exactly** as it appears in the image. It is extremely important that the generated HTML, CSS, and JavaScript, when combined, will produce a visual output that is as close as possible to the original screenshot. Do not omit any important elements or styling details. Pay very close attention to fonts, colors, spacing, layout, and interactive elements. Preserve the original structure and avoid making unnecessary changes.

    If there are buttons, forms, or other interactive elements, create JavaScript code that makes them functional or simulates the expected behavior.

    Return your response in the following JSON format, enclosed within `==START_JSON==` and `==STOP_JSON==` tokens:

    ==START_JSON==
    {
        "files": {
            "index.html": {"content": "<html>...</html>"},
            "style.css": {"content": "body { ... }"},
            "script.js": {"content": "console.log('...');"},
            "and_any_other_files.needed": {"content": "file content"}
        }
    }
    ==STOP_JSON==

    Include **all** necessary files to recreate the webpage, including HTML, CSS (either inline or in separate `.css` files), JavaScript (either inline or in separate `.js` files), and any other assets (images, fonts, etc.) if present in the screenshot.  Name the main HTML file `index.html`. Use `style.css` for CSS and `script.js` for JavaScript, unless there are multiple CSS or JavaScript files, in which case, name them descriptively (e.g., `responsive.css`, `animations.js`). Prioritize external CSS and JS files for better organization unless inline styling or scripting is clearly more appropriate based on the screenshot.

    Generate JavaScript code to handle basic interactions, such as button clicks, form submissions, and any other dynamic behavior visible in the screenshot.

    It is CRITICAL that the HTML is well-formed and valid, that CSS styles are complete and accurate, and that JavaScript code functions as intended to produce the webpage shown in the screenshot.  **The appearance and functionality of the outputted code should match the screenshot, and this is your primary objective.**

    DO NOT INCLUDE any introductory or explanatory text outside of the JSON block. Only the JSON block should be present in your response.
    """

    model = genai.GenerativeModel(model_name)
    contents = [prompt, {"mime_type": "image/png", "data": img_str}]

    try:
        socketio.emit('progress', {'percent': 50, 'message': 'Sending request to Gemini...'})
        response = model.generate_content(contents)
        socketio.emit('progress', {'percent': 75, 'message': 'Received response from Gemini'})
        app.logger.info(f"Raw Gemini API response: {response.text}")
        files = parse_model_response(response.text)

        # Extract file contents
        css_content = files["files"].get("style.css", {}).get("content", "")
        js_content = files["files"].get("script.js", {}).get("content", "")
        index_html_content = files["files"].get("index.html", {}).get("content", "")

        # Strip index.html to body content for combined HTML
        body_content = index_html_content
        body_match = re.search(r"<body[^>]*>(.*?)</body>", index_html_content, re.DOTALL | re.IGNORECASE)
        if body_match:
            body_content = body_match.group(1).strip()
            app.logger.info("Extracted body content for combined HTML")
        elif "<html" in index_html_content.lower():
            content_match = re.search(r"<body[^>]*>(.*)$", index_html_content, re.DOTALL | re.IGNORECASE) or \
                            re.search(r"(?<=<html[^>]*>).*", index_html_content, re.DOTALL | re.IGNORECASE)
            body_content = content_match.group(0).strip() if content_match else index_html_content
            app.logger.warning("No explicit body tag found, using inferred content for combined HTML")
        else:
            app.logger.info("No HTML structure detected, using raw content as-is for combined HTML")

        # Build combined HTML with inlined CSS and JS
        combined_html = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
        combined_html += "    <meta charset=\"UTF-8\">\n"
        combined_html += "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
        combined_html += "    <title>Recreated Webpage</title>\n"
        if css_content:
            combined_html += "    <style>\n"
            combined_html += f"        {css_content}\n"
            combined_html += "    </style>\n"
        combined_html += "</head>\n<body>\n"
        
        if body_content:
            combined_html += f"    {body_content}\n"
        else:
            combined_html += "    <!-- No HTML content generated -->\n"

        if js_content:
            combined_html += "    <script>\n"
            combined_html += f"        {js_content}\n"
            combined_html += "    </script>\n"
        combined_html += "</body>\n</html>"

        # For preview and standalone file, use the original full index.html content
        html_content = index_html_content if index_html_content else "<p>No HTML file generated</p>"
        if not index_html_content:
            html_content = next(
                (f["content"] for fname, f in files["files"].items() if fname.endswith(".html")),
                "<p>No HTML file generated</p>"
            )

        app.logger.info("HTML and files extracted successfully")
        socketio.emit('progress', {'percent': 100, 'message': 'Processing complete'})
        return files, html_content, combined_html
    except Exception as e:
        app.logger.error(f"Error extracting files: {e}")
        socketio.emit('progress', {'percent': 100, 'message': f'Error: {str(e)}'})
        return {"files": {"error.txt": {"content": f"Error: {str(e)}"}}}, f"Error: {str(e)}", f"Error: {str(e)}"

# --- Flask Routes ---
@app.route('/')
def index():
    return render_template('index.html', models=AVAILABLE_MODELS, default_model=DEFAULT_MODEL)

@app.route('/process_url', methods=['POST'])
def process_url():
    data = request.form
    url = data.get('url')
    max_height = int(data.get('max_height', DEFAULT_MAX_HEIGHT))
    image_format = data.get('image_format', DEFAULT_IMAGE_FORMAT)
    timeout = int(data.get('timeout', DEFAULT_TIMEOUT))
    model_name = data.get('model_name', DEFAULT_MODEL)

    try:
        files, html_content, combined_html = image_to_html(screenshot_from_url(url, max_height, image_format, timeout), model_name)
        return jsonify({"files": files["files"], "preview": html_content, "combined_html": combined_html})
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.route('/process_image', methods=['POST'])
def process_image():
    if 'image' not in request.files:
        return jsonify({"error": "No image uploaded"}), 400

    image_file = request.files['image']
    model_name = request.form.get('model_name', DEFAULT_MODEL)

    try:
        files, html_content, combined_html = image_to_html(Image.open(image_file), model_name)
        return jsonify({"files": files["files"], "preview": html_content, "combined_html": combined_html})
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@socketio.on('connect')
def test_connect():
    app.logger.info("Client connected")

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=7860, debug=False, allow_unsafe_werkzeug=True)