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:
parent
ae53ffe451
commit
5d1941d280
159
llm_team_ui.py
159
llm_team_ui.py
@ -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():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user