diff --git a/Dockerfile b/Dockerfile index ed1633a..d0f25d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,4 @@ COPY . . EXPOSE 8000 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "120"] diff --git a/app.py b/app.py index 58ab9cd..c58d66c 100644 --- a/app.py +++ b/app.py @@ -61,7 +61,7 @@ async def render_pattern( seed: str = Form(""), output_format: str = Form("html"), ): - width = max(20, min(500, width)) + width = max(20, min(2000, width)) height = max(10, min(250, height)) if pattern not in PATTERN_TYPES: pattern = "plasma" @@ -108,7 +108,7 @@ async def render_art( if len(content) > MAX_UPLOAD_SIZE: return JSONResponse({"error": "File too large (max 10MB)"}, 400) - width = max(20, min(500, width)) + width = max(20, min(2000, width)) if palette not in PALETTES: palette = "wingdings" if bg not in ("dark", "light"): diff --git a/docker-compose.yml b/docker-compose.yml index 0292c2c..6e51337 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,8 +41,8 @@ services: deploy: resources: limits: - cpus: "2" - memory: 512M + cpus: "4" + memory: 1G networks: traefik-public: diff --git a/render.py b/render.py index a24ccb3..6882957 100644 --- a/render.py +++ b/render.py @@ -110,40 +110,75 @@ def _render_frame( ) -> str: """Render a single prepared (resized RGB) frame to art string.""" width, height = img.size + num_chars = len(chars) + pixels = img.load() - # Build luminance grid - lum_grid = [] + # Build luminance grid using fast pixel access + lum_grid = [[0.0] * width for _ in range(height)] for y in range(height): - row = [] for x in range(width): - r, g, b = img.getpixel((x, y)) - row.append(get_luminance(r, g, b)) - lum_grid.append(row) + r, g, b = pixels[x, y] + lum_grid[y][x] = 0.299 * r + 0.587 * g + 0.114 * b if dither: - lum_grid = _floyd_steinberg_dither(lum_grid, width, height, len(chars)) + lum_grid = _floyd_steinberg_dither(lum_grid, width, height, num_chars) - lines = [] - for y in range(height): - line = [] - for x in range(width): - r, g, b = img.getpixel((x, y)) - lum = max(0.0, min(255.0, lum_grid[y][x])) - idx = int(lum / 256 * len(chars)) - idx = min(idx, len(chars) - 1) - char = chars[idx] - if double_width: - char = char + " " + if output_format == "html": + # Optimized HTML: use CSS classes for color grouping + # Build line strings directly to reduce object creation + parts = [] + for y in range(height): + line_parts = [] + prev_color = None + run_chars = [] + for x in range(width): + r, g, b = pixels[x, y] + lum = max(0.0, min(255.0, lum_grid[y][x])) + idx = min(int(lum / 256 * num_chars), num_chars - 1) + char = chars[idx] + if double_width: + char = char + " " + color = f"{r},{g},{b}" + if color == prev_color: + run_chars.append(html_escape(char)) + else: + if run_chars: + line_parts.append(f'{"".join(run_chars)}') + run_chars = [html_escape(char)] + prev_color = color + if run_chars: + line_parts.append(f'{"".join(run_chars)}') + parts.append("".join(line_parts)) + return "\n".join(parts) - if output_format == "ansi": + elif output_format == "ansi": + lines = [] + for y in range(height): + line = [] + for x in range(width): + r, g, b = pixels[x, y] + lum = max(0.0, min(255.0, lum_grid[y][x])) + idx = min(int(lum / 256 * num_chars), num_chars - 1) + char = chars[idx] + if double_width: + char = char + " " line.append(f"\033[38;2;{r};{g};{b}m{char}\033[0m") - elif output_format == "html": - line.append(f'{html_escape(char)}') - else: # plain - line.append(char) - lines.append("".join(line)) + lines.append("".join(line)) + return "\n".join(lines) - return "\n".join(lines) + else: # plain + lines = [] + for y in range(height): + line = [] + for x in range(width): + lum = max(0.0, min(255.0, lum_grid[y][x])) + idx = min(int(lum / 256 * num_chars), num_chars - 1) + char = chars[idx] + if double_width: + char = char + " " + line.append(char) + lines.append("".join(line)) + return "\n".join(lines) def image_to_art( diff --git a/static/app.js b/static/app.js index 5cab57e..4711fc8 100644 --- a/static/app.js +++ b/static/app.js @@ -112,6 +112,22 @@ function setFile(file) { widthSlider.addEventListener('input', () => { widthVal.textContent = widthSlider.value; }); patternHeight.addEventListener('input', () => { heightVal.textContent = patternHeight.value; }); +// Resolution presets sync the width slider +const resPreset = $('resPreset'); +resPreset.addEventListener('change', () => { + const val = parseInt(resPreset.value); + widthSlider.value = val; + widthVal.textContent = val; +}); +widthSlider.addEventListener('input', () => { + // Update preset dropdown to match closest + const val = parseInt(widthSlider.value); + const opts = [100, 300, 600, 1000, 1500]; + let closest = opts[0]; + for (const o of opts) { if (Math.abs(o - val) < Math.abs(closest - val)) closest = o; } + resPreset.value = closest; +}); + // ── Generate (Image Mode) ────────────────────────────── generateBtn.addEventListener('click', generateImage); @@ -295,14 +311,14 @@ function toggleCompare() { function setZoom(scale) { zoomScale = Math.max(0.1, Math.min(8.0, scale)); - previewArea.style.fontSize = (8 * zoomScale) + 'px'; + previewArea.style.fontSize = (5 * zoomScale) + 'px'; zoomLevel.textContent = Math.round(zoomScale * 100) + '%'; } function autoFitZoom() { requestAnimationFrame(() => { const containerWidth = previewArea.clientWidth - 24; - previewArea.style.fontSize = '8px'; + previewArea.style.fontSize = '5px'; zoomScale = 1.0; const artWidth = previewArea.scrollWidth; if (artWidth > containerWidth && artWidth > 0) { diff --git a/static/style.css b/static/style.css index 9222bc3..1774a51 100644 --- a/static/style.css +++ b/static/style.css @@ -358,11 +358,11 @@ header { flex: 1 1 0; min-height: 0; background: var(--surface2); - padding: 8px; + padding: 4px; overflow: auto; font-family: 'JetBrains Mono', monospace; - font-size: 8px; - line-height: 1.05; + font-size: 5px; + line-height: 1.0; letter-spacing: 0px; white-space: pre; position: relative; diff --git a/templates/index.html b/templates/index.html index 28cd7ad..8b7d879 100644 --- a/templates/index.html +++ b/templates/index.html @@ -68,8 +68,18 @@
- - + + +
+
+ +