From 303c83e86a3fc7f78bcf4e62a4f8176b528f6f45 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 9 Jun 2026 13:40:14 -0500 Subject: [PATCH] feat(ai): generate background + foreground images for signs (#41 Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prompt now produces a full sign: the LLM writes the design AND image prompts, the server generates the images and composites them with the crisp text layer. - lib/image-gen.js: text-to-image with 3 BYO/self-hostable backends, all behind the SSRF guard: 'sdcpp' (local stable-diffusion.cpp OpenAI-compatible server, exact small sizes that fit VRAM), 'openai' (cloud / OpenAI-compatible, snapped sizes), 'comfyui' (prompt/history/view API). - ai.js: prompt asks for a background_prompt (preferred — full-bleed atmosphere) and an optional foreground image element; after the design is normalized, the bg + fg images are generated best-effort (a failed image never fails the sign) and returned as data URLs. New image_* settings (provider/base_url/model), image_provider whitelist, schema column + migration. - designer.js: AI-images section in settings; generate applies the background image; publish bakes the background image into the HTML so it survives. - server.js: raise JSON body limit to 12mb for embedded image data URLs. Verified end-to-end on local Vulkan SDXL (RTX 5090): prompt -> bg+fg images on the canvas -> publish creates a widget with the images embedded. 63/63. Note: prod (not self-hosted) requires a PUBLIC image endpoint (e.g. OpenAI); the SSRF guard blocks localhost there. Follow-up: upload generated images to the content store and reference by URL to avoid multi-MB widget configs. --- frontend/js/i18n/en.js | 10 +++- frontend/js/views/designer.js | 30 +++++++++- server/db/database.js | 2 + server/db/schema.sql | 1 + server/lib/image-gen.js | 109 ++++++++++++++++++++++++++++++++++ server/routes/ai.js | 87 +++++++++++++++++++++------ server/server.js | 5 +- 7 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 server/lib/image-gen.js diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 8e39130..065f07c 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -605,8 +605,16 @@ export default { 'designer.ai.placeholder': "Describe your sign — e.g. 'Summer sale, 20% off mains, bright modern'", 'designer.ai.generate': 'Generate design', 'designer.ai.generating': 'Generating…', - 'designer.ai.contacting': 'Contacting your AI endpoint… (local models can be slow)', + 'designer.ai.contacting': 'Generating… text is quick; images add ~10–30s', 'designer.ai.done': 'Generated {n} element(s) — tweak and Publish.', + 'designer.ai.done_imgwarn': 'Generated {n} element(s) — an image couldn’t be generated (text is ready). Publish.', + 'designer.ai.images_title': 'AI images (optional)', + 'designer.ai.images_desc': 'Generate a background (and a foreground graphic) from your prompt. Point at a local sd.cpp / ComfyUI server or OpenAI. Off = text + shapes only.', + 'designer.ai.image_provider': 'Image provider', + 'designer.ai.image_off': 'Off (text + shapes only)', + 'designer.ai.image_base_url': 'Image endpoint URL', + 'designer.ai.image_model': 'Image model', + 'designer.ai.image_model_ph': 'optional — e.g. dall-e-3; blank for sd.cpp / ComfyUI', 'designer.ai.failed': 'Generation failed', 'designer.ai.need_prompt': 'Enter a prompt first', 'designer.ai.settings_title': 'AI design settings', diff --git a/frontend/js/views/designer.js b/frontend/js/views/designer.js index 5d018ef..9672ebe 100644 --- a/frontend/js/views/designer.js +++ b/frontend/js/views/designer.js @@ -136,13 +136,18 @@ export function render(container) { try { const design = await api.aiGenerateDesign(prompt); elements = []; selectedIdx = -1; - if (design.background) { + if (design.backgroundImage) { + bgImageDataUrl = design.backgroundImage; // AI-generated backdrop + if (design.background) bgValue = design.background; // kept as fallback + } else if (design.background) { bgValue = design.background; bgImageDataUrl = null; const bc = document.getElementById('bgColor'); if (bc) bc.value = design.background; } (design.elements || []).forEach(el => elements.push(el)); redraw(); - status.textContent = t('designer.ai.done', { n: (design.elements || []).length }); + status.textContent = design.image_warning + ? t('designer.ai.done_imgwarn', { n: (design.elements || []).length }) + : t('designer.ai.done', { n: (design.elements || []).length }); } catch (err) { status.textContent = (err && err.message) || t('designer.ai.failed'); } finally { @@ -340,6 +345,21 @@ async function openAiSettings() {
${t('designer.ai.key_hint')}
+ +
+

${t('designer.ai.images_title')}

+

${t('designer.ai.images_desc')}

+
+
+
+
+
+