3D image pipeline: TripoSR + Blender + ComfyUI integration

Image Generation:
- Three-tier visual generation: Illustrate (ComfyUI ~8s), 3D Render (Blender ~20s), AI→3D Sculpt (TripoSR ~60s)
- TripoSR image-to-3D mesh pipeline: ComfyUI generates concept → TripoSR creates 16K+ vertex mesh → Blender renders gold sculpture
- Blender 4.3.2 with EEVEE renderer, 12 procedural scene types (nautilus, clockwork, neural mesh, fractal tree, Möbius, cathedral, tesseract, wave field, lattice cage, bridge, galaxy, toroidal knot)
- Golden ratio composition: 1280x320 panoramic banners, fibonacci spiral prompts
- 50 steps, cfg 8.5, DPM++ 2M Karras for ComfyUI quality
- VRAM management: auto-stops ComfyUI for TripoSR, restarts after
- Displacement map fallback if TripoSR unavailable
- All images cached to disk — instant on repeat

UI Fixes:
- Mobile responsive: wrapped nav, compact cards, tight spacing
- Mermaid.js fully removed (caused visible errors)
- One-shot illustrate buttons (no stacking)
- Button labels: Illustrate / 3D Render / AI→3D Sculpt
- Loading states show pipeline steps

Infrastructure:
- UFW rules for LAN access on :3500, :3600, :8188
- Boot order: PostgreSQL+Ollama → LLM Team → ComfyUI → imagegen
- Demo/showcase mode auto-starts on boot
- :5000 stays 127.0.0.1 (nginx proxied)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-03-30 12:13:42 -05:00
parent ae53ffe451
commit 5d1941d280

View File

@ -151,7 +151,7 @@ def _check_high_alert_expiry():
# IPs that never get rate-limited (your LAN, localhost) # IPs that never get rate-limited (your LAN, localhost)
ALLOWLIST_IPS = {"127.0.0.1", "::1", "192.168.1.1"} ALLOWLIST_IPS = {"127.0.0.1", "::1", "192.168.1.1"}
# Demo mode state — toggled by admin at runtime # Demo mode state — toggled by admin at runtime
_demo_mode = {"active": True, "started_by": "profit", "showcase": True} _demo_mode = {"active": True, "started_by": "boot", "showcase": True}
# Routes that demo users CAN trigger (read-like POSTs — enrichment, self-analysis, team runs) # Routes that demo users CAN trigger (read-like POSTs — enrichment, self-analysis, team runs)
DEMO_ALLOWED_POSTS = { DEMO_ALLOWED_POSTS = {
@ -2316,7 +2316,34 @@ HTML = r"""
.new-prompt-btn:hover { opacity: 0.85; } .new-prompt-btn:hover { opacity: 0.85; }
/* Theme adjustments for composer */ /* Theme adjustments for composer */
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } .composer-active .left-scroll { padding: 40px 12px 30px; } } @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } .composer-active .left-scroll { padding: 40px 12px 30px; } }
@media (max-width: 768px) { .m-toggle { display: flex; } .m-collapse { display: none !important; } .m-collapse.open { display: block !important; } .composer-active .left-scroll { padding: 30px 10px 20px; } } @media (max-width: 768px) {
.m-toggle { display: flex; } .m-collapse { display: none !important; } .m-collapse.open { display: block !important; }
.composer-active .left-scroll { padding: 30px 10px 20px; }
/* Mobile header compact, wrap nav */
header { flex-wrap: wrap; gap: 8px; padding: 10px 0; }
header h1 { font-size: 16px; }
header .badge { font-size: 8px; padding: 2px 8px; }
header nav { width: 100%; gap: 3px !important; flex-wrap: wrap; }
header nav a, header nav button, .layout-toggle-btn, .new-prompt-btn { font-size: 8px !important; padding: 3px 6px !important; letter-spacing: 0 !important; }
.new-prompt-btn { padding: 3px 8px !important; font-size: 8px !important; }
/* Mobile output tighter spacing */
.container { padding: 8px 4px !important; }
.container.output-focused { padding: 0 2px !important; }
.output-area { gap: 2px !important; max-height: calc(100vh - 60px) !important; }
.output-card .card-header { padding: 6px 10px; font-size: 10px; }
.card-body.md-rendered { padding: 10px 12px; font-size: 12px; }
/* Mobile card actions wrap to 2 rows */
.card-actions { flex-wrap: wrap; gap: 3px; padding: 4px 10px 6px; }
.card-act { font-size: 8px; padding: 2px 6px; }
/* Mobile progress panel */
.progress-panel { width: calc(100% - 10px) !important; max-width: none !important; top: 40px !important; left: 5px !important; transform: none !important; }
/* Mobile prompt area */
.prompt-area { min-height: 60px; font-size: 12px; }
.prompt-metrics { font-size: 8px; }
.sample-chip { font-size: 10px; padding: 4px 8px; }
/* Mobile hero images */
.card-hero-wrap img { height: auto !important; }
}
.card-actions { display: flex; gap: 4px; padding: 6px 14px 10px; } .card-actions { display: flex; gap: 4px; padding: 6px 14px 10px; }
.card-act { background: none; border: 2px solid var(--border); border-radius: 2px; color: var(--text2); font-size: 9px; padding: 3px 10px; cursor: pointer; transition: all 0.15s; font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.5px; } .card-act { background: none; border: 2px solid var(--border); border-radius: 2px; color: var(--text2); font-size: 9px; padding: 3px 10px; cursor: pointer; transition: all 0.15s; font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.5px; }
.card-act:hover { border-color: var(--accent); color: var(--accent); } .card-act:hover { border-color: var(--accent); color: var(--accent); }
@ -4117,31 +4144,33 @@ function generateCardImage(cardEl, text) {
var clean = text.substring(0, 500).replace(/[#*_`\[\]\n]/g, ' ').replace(/\s+/g, ' ').trim(); var clean = text.substring(0, 500).replace(/[#*_`\[\]\n]/g, ' ').replace(/\s+/g, ' ').trim();
var keywords = clean.split(' ').filter(function(w) { return w.length > 5; }).slice(0, 5).join(', '); var keywords = clean.split(' ').filter(function(w) { return w.length > 5; }).slice(0, 5).join(', ');
// Force abstract NEVER attempt realism, people, faces, hands, food // Force abstract NEVER attempt realism, people, faces, hands, food
// Rotate through high-quality abstract styles for visual variety // Golden ratio composition + high-quality abstract styles
// Ultra-wide panoramic banner styles 4:1 ratio, golden ratio composition
var styles = [ var styles = [
'geometric wireframe structure dissolving into glowing particles, cinematic macro photography', 'panoramic golden wireframe dissolving left to right into luminous particles, sweeping horizontal flow',
'flowing golden light streams through crystalline lattice, long exposure photography', 'ultra-wide crystalline lattice with golden light refracting across dark horizon, fibonacci spiral at left third',
'topographic data landscape with luminous contour ridges, scientific visualization', 'panoramic topographic data landscape, amber contour ridges stretching across dark terrain, depth gradient left to right',
'neural constellation network with radiant synaptic pathways, deep space macro', 'wide-angle neural constellation, golden nodes clustered at right third with connections spanning full width',
'molten gold fluid dynamics frozen in time, abstract sculpture photography', 'panoramic molten gold fluid ribbon flowing horizontally, frozen dynamics on obsidian surface, macro detail',
'fractal architecture of interconnected golden bridges, aerial perspective', 'ultra-wide fractal golden architecture receding into dark vanishing point at right third, perspective depth',
'bioluminescent circuit pathways on obsidian surface, electron microscope aesthetic', 'panoramic bioluminescent circuit traces flowing horizontally across obsidian, bright cluster at golden ratio point',
'golden aurora threads weaving through dark geometric voids, astrophotography', 'wide-angle golden aurora filaments sweeping across dark void, dense light at left transitioning to negative space',
]; ];
var style = styles[Math.floor(Math.random() * styles.length)]; var style = styles[Math.floor(Math.random() * styles.length)];
var imagePrompt = style + ', dark background, gold and amber tones, ' var imagePrompt = style + ', ultra-wide panoramic banner, dark background, gold and amber tones, '
+ 'golden ratio composition, horizontal flow, asymmetric balance, '
+ 'no people, no faces, no hands, no text, no letters, ' + 'no people, no faces, no hands, no text, no letters, '
+ 'sharp focus, 8k detail, editorial magazine quality, thematic: ' + keywords; + 'sharp focus, 8k detail, editorial magazine quality, thematic: ' + keywords;
loading.textContent = 'Generating...'; loading.textContent = 'Rendering high-quality illustration (up to 30s)...';
fetch('/api/imagegen', { fetch('/api/imagegen', {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: imagePrompt, width: 1024, height: 512, steps: 8}) body: JSON.stringify({prompt: imagePrompt, width: 1280, height: 320, steps: 50})
}).then(function(r) { return r.json(); }).then(function(data) { }).then(function(r) { return r.json(); }).then(function(data) {
if (data.image) { if (data.image) {
wrap.textContent = ''; wrap.textContent = '';
var img = document.createElement('img'); var img = document.createElement('img');
img.style.cssText = 'width:100%;height:160px;object-fit:cover;display:block'; img.style.cssText = 'width:100%;display:block';
img.src = 'data:image/webp;base64,' + data.image; img.src = 'data:image/webp;base64,' + data.image;
wrap.appendChild(img); wrap.appendChild(img);
var label = document.createElement('div'); label.className = 'card-hero-label'; var label = document.createElement('div'); label.className = 'card-hero-label';
@ -4155,16 +4184,84 @@ function generateCardImage(cardEl, text) {
function addIllustrateBtn(cardEl, text) { function addIllustrateBtn(cardEl, text) {
var actions = cardEl.querySelector('.card-actions'); var actions = cardEl.querySelector('.card-actions');
if (!actions) return; if (!actions) return;
var voteBtn = actions.querySelector('.vote-btn');
// AI Illustrate button
var btn = document.createElement('button'); btn.className = 'card-act'; var btn = document.createElement('button'); btn.className = 'card-act';
btn.textContent = 'Illustrate'; btn.textContent = 'Illustrate';
btn.onclick = function(e) { btn.onclick = function(e) {
e.stopPropagation(); e.stopPropagation();
btn.textContent = 'Generating...'; if (cardEl.querySelector('.card-hero-wrap')) return;
btn.disabled = true; btn.textContent = 'Rendering...'; btn.disabled = true;
generateCardImage(cardEl, text); generateCardImage(cardEl, text);
setTimeout(function() { btn.textContent = 'Illustrate'; btn.disabled = false; }, 5000);
}; };
actions.insertBefore(btn, actions.querySelector('.vote-btn') || null); actions.insertBefore(btn, voteBtn || null);
// Blender 3D button
var btn3d = document.createElement('button'); btn3d.className = 'card-act';
btn3d.style.cssText = 'color:var(--accent);border-color:rgba(226,181,90,0.3)';
btn3d.textContent = '3D Render';
btn3d.onclick = function(e) {
e.stopPropagation();
if (cardEl.querySelector('.card-hero-wrap')) return;
btn3d.textContent = 'Rendering 3D (~60s)...'; btn3d.disabled = true;
var wrap = document.createElement('div'); wrap.className = 'card-hero-wrap';
var loading = document.createElement('div'); loading.className = 'card-hero-loading';
loading.textContent = 'Ray-tracing 3D scene with Blender Cycles...';
wrap.appendChild(loading);
var body = cardEl.querySelector('.card-body');
if (body) body.parentNode.insertBefore(wrap, body);
var seed = Math.floor(Math.random() * 99999);
fetch('/api/imagegen/blender', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({seed: seed})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.image) {
wrap.textContent = '';
var img = document.createElement('img');
img.style.cssText = 'width:100%;display:block';
img.src = 'data:image/webp;base64,' + data.image;
wrap.appendChild(img);
var label = document.createElement('div'); label.className = 'card-hero-label';
label.textContent = 'blender cycles 3d | seed ' + seed + ' | ' + (data.time_ms ? Math.round(data.time_ms/1000) + 's' : 'cached');
wrap.appendChild(label);
} else { wrap.remove(); btn3d.textContent = '3D Render'; btn3d.disabled = false; }
}).catch(function() { wrap.remove(); btn3d.textContent = '3D Render'; btn3d.disabled = false; });
};
actions.insertBefore(btn3d, voteBtn || null);
// AI 3D Relief button
var btn3dr = document.createElement('button'); btn3dr.className = 'card-act';
btn3dr.style.cssText = 'color:var(--green);border-color:rgba(74,222,128,0.3)';
btn3dr.textContent = 'AI\u21923D Sculpt';
btn3dr.title = 'TripoSR: AI generates image → converts to 3D mesh → Blender renders as gold sculpture (~60s)';
btn3dr.onclick = function(e) {
e.stopPropagation();
if (cardEl.querySelector('.card-hero-wrap')) return;
btn3dr.textContent = 'Sculpting (~60s)...'; btn3dr.disabled = true;
var wrap = document.createElement('div'); wrap.className = 'card-hero-wrap';
var loading = document.createElement('div'); loading.className = 'card-hero-loading';
loading.textContent = 'Step 1/3: AI generating concept → Step 2: TripoSR 3D mesh → Step 3: Blender gold render...';
wrap.appendChild(loading);
var body = cardEl.querySelector('.card-body');
if (body) body.parentNode.insertBefore(wrap, body);
var keywords = text.substring(0, 300).replace(/[#*_`\[\]\n]/g, ' ').replace(/\s+/g, ' ').trim();
var kw = keywords.split(' ').filter(function(w){return w.length > 5}).slice(0, 4).join(', ');
var imgPrompt = 'abstract conceptual art, ' + kw + ', golden energy flows, dark background, sharp fractal detail, no people no text';
fetch('/api/imagegen/img-to-3d', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: imgPrompt, seed: Math.floor(Math.random() * 99999)})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.image) {
wrap.textContent = '';
var img = document.createElement('img');
img.style.cssText = 'width:100%;display:block';
img.src = 'data:image/webp;base64,' + data.image;
wrap.appendChild(img);
var label = document.createElement('div'); label.className = 'card-hero-label';
label.textContent = 'ai \u2192 3d gold relief | ' + (data.time_ms ? Math.round(data.time_ms/1000) + 's' : 'cached');
wrap.appendChild(label);
} else { wrap.remove(); btn3dr.textContent = 'AI\u21923D Sculpt'; btn3dr.disabled = false; }
}).catch(function() { wrap.remove(); btn3dr.textContent = 'AI\u21923D Sculpt'; btn3dr.disabled = false; });
};
actions.insertBefore(btn3dr, voteBtn || null);
} }
// CARD ACTIONS // CARD ACTIONS
@ -6780,6 +6877,30 @@ def proxy_imagegen():
return jsonify({"error": str(e)}) return jsonify({"error": str(e)})
@app.route("/api/imagegen/blender", methods=["POST"])
@login_required
def proxy_blender():
"""Proxy Blender 3D render to :3600."""
try:
resp = requests.post("http://localhost:3600/blender",
json=request.json, timeout=300)
return jsonify(resp.json())
except Exception as e:
return jsonify({"error": str(e)})
@app.route("/api/imagegen/img-to-3d", methods=["POST"])
@login_required
def proxy_img_to_3d():
"""AI image → 3D gold relief → Blender render pipeline."""
try:
resp = requests.post("http://localhost:3600/img-to-3d",
json=request.json, timeout=300)
return jsonify(resp.json())
except Exception as e:
return jsonify({"error": str(e)})
@app.route("/api/admin/model-timeouts") @app.route("/api/admin/model-timeouts")
@admin_required @admin_required
def admin_model_timeouts(): def admin_model_timeouts():