From 94419d767304d19e95b25581353df4f6587d58a2 Mon Sep 17 00:00:00 2001 From: mike Date: Wed, 1 Jul 2026 05:42:20 +0200 Subject: [PATCH] updates UI --- backlog.md | 4 + tour-comfy/car.html | 340 +++++++++++++++++++++++++++++++---------- tour-comfy/database.py | 3 +- tour-comfy/edit_api.py | 32 +++- tour-comfy/poses.md | 14 +- 5 files changed, 298 insertions(+), 95 deletions(-) diff --git a/backlog.md b/backlog.md index 185db24..8184c07 100644 --- a/backlog.md +++ b/backlog.md @@ -97,6 +97,10 @@ Scenery: - in order to decouple the landingpage/dashboard from the studio view (editor) we are going to generate group "shoot" specific html pages. Start by just generate the studio portion of car.html into the group specifc html pages. It does not need to be synchronous, but when data of the group is changing we should update the group specifc data json file too, we copy the goup files to the output folder next to car.html - in /mnt/zim/tour-comfy/output/_turntable we have data regarding orbit generations. We want to store the orbit data in the database, start by adding the corresponding tags and meta info to the images (currently stored in the person table (but actually are images with metadata)) - in the ui - studio view we have an orbit filter. Currently the filter is filtering too many files, also references of orbit files are being marked orbit. When an orbit file is used, the metadata should not include the orbit tag. +- pose-prompt has at least three parts, [scenery ,pose, art-form] allow in the ui to compound the prompt with these, scene default = "You are in a black void" art-form= "photo-realistic keep identical person from reference" also allow inherit scenery to as option. + + + ## refine #file:///mnt/zim/tour-comfy/output/shoot_cg_7ec17537.html - when refresh page, we lose track of current jobs running. diff --git a/tour-comfy/car.html b/tour-comfy/car.html index 9e77d0b..11411e9 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -1330,10 +1330,20 @@ inset: 0; background: #0a0a0a; z-index: 500; - flex-direction: column; + flex-direction: row; /* row layout to support full height sidebar */ + overflow: hidden; } .studio.open { display: flex; } + .studio-left { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + height: 100%; + overflow: hidden; + } + .studio-topbar { flex-shrink: 0; display: flex; @@ -1348,10 +1358,6 @@ .studio-topbar-center{ flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; overflow: hidden; } .studio-topbar-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } - .studio-body { - flex: 1; min-height: 0; - display: flex; overflow: hidden; - } .studio-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; @@ -1464,7 +1470,7 @@ .sidebar-inner { flex: 1; min-height: 0; display: flex; flex-direction: column; - padding-top: 36px; overflow: hidden; + padding-top: 8px; overflow: hidden; transition: opacity 0.15s; } .studio-sidebar.collapsed .sidebar-inner { opacity: 0; pointer-events: none; } @@ -1936,17 +1942,6 @@ - -
-
- - API -
-
- - Model -
-
@@ -2069,12 +2064,23 @@
-
+
0 images πŸ—‘οΈ Trash + +
+
+ + API +
+
+ + COMFY +
+
@@ -2103,30 +2109,65 @@
- -
-
- - -
-
- - - - -
-
- - -
-
+ +
+ +
+
+ + + +
+
+ + + / + + + +
+ + +
- -
+ + + +
+
+ +
+
+ + API +
+
+ + COMFY +
+
+ + +
+
@@ -2179,19 +2220,20 @@
-
+
+
- +
-
@@ -2503,6 +2544,36 @@ // --- HYDRATION_START --- const PRELOADED_IMAGES = [ + "20260701_051754_image.png", + "20260701_051737_image.png", + "20260701_051721_image.png", + "20260701_051704_image.png", + "20260701_051647_image.png", + "20260701_051614_image.png", + "20260701_051541_image.png", + "20260701_051520_image.png", + "20260701_050249_image.png", + "20260701_050232_image.png", + "20260701_050215_image.png", + "20260701_050158_image.png", + "20260701_050141_image.png", + "20260701_050124_image.png", + "20260701_050108_image.png", + "20260701_050051_image.png", + "20260701_050035_image.png", + "20260701_050018_image.png", + "20260701_050001_image.png", + "20260701_045945_image.png", + "20260701_045928_image.png", + "20260701_045912_image.png", + "20260701_045838_image.png", + "20260701_045822_image.png", + "20260701_045805_image.png", + "20260701_045749_image.png", + "20260701_045732_image.png", + "20260701_045716_image.png", + "20260701_045659_1_20260619_202924_image.png", + "20260620_014953_image.png", "20260630_130454_sc_0_20260622_101128_image.nobg.png", "20260630_131638_sc_2_20260622_101128_image.nobg.nobg.png", "20260630_151502_sc_0_20260622_101128_image.nobg.png", @@ -7232,7 +7303,6 @@ "20260620_020313_2_20260618_172500_image.png", "20260620_020302_2_20260618_172500_image.png", "20260620_020251_2_20260618_172500_image.png", - "20260620_014953_image.png", "20260620_014646_image.png", "20260620_014349_image.png", "20260620_014239_img_77.png", @@ -7943,6 +8013,7 @@ // ============================================ let autoRefreshTimer = null; + let isInitialLoad = true; let knownFiles = new Set(); let currentFiles = []; let currentGroups = []; @@ -8175,6 +8246,7 @@ // --- studio view (replaces lightbox) --- let lbUrls = [], lbNames = [], lbIdx = 0; + let _lastRenderedFilename = null; let _activeFilmstripTab = 'active'; // 'active', 'group', 'hidden', 'source', 'archived' let _multiSelectModeActive = false; const _selectedFilenames = new Set(); @@ -9123,6 +9195,23 @@ function updateStudio() { const fname = lbNames[lbIdx]; + if (fname && fname !== _lastRenderedFilename) { + // Image changed, force cancel any active text areas/edits + _lastRenderedFilename = fname; + const genPromptEl = document.getElementById('lbGenPrompt'); + if (genPromptEl) { + genPromptEl.innerHTML = ''; + genPromptEl.onclick = () => editMetadataField('prompt'); + } + const descEl = document.getElementById('lbDescription'); + if (descEl) { + descEl.innerHTML = ''; + descEl.onclick = () => editMetadataField('description'); + } + } else if (!fname) { + _lastRenderedFilename = null; + } + if (document.getElementById('poseCanvas')) _removePoseOverlay(); // skeleton is per-image document.getElementById('poseResults')?.remove(); @@ -9256,6 +9345,9 @@ const nameInput = document.getElementById('lbNameInput'); if (nameInput) { nameInput.value = imageNames[fname] || ''; nameInput.placeholder = fname; } + const topbarNameInput = document.getElementById('lbTopbarImageNameInput'); + if (topbarNameInput) { topbarNameInput.value = imageNames[fname] || ''; topbarNameInput.placeholder = fname; } + const clipEl = document.getElementById('lbClipDesc'); if (clipEl) clipEl.textContent = clipDescriptions[fname] || ''; @@ -9682,6 +9774,8 @@ }); if (r.ok) { imageNames[fname] = newName; + const topbarNameInput = document.getElementById('lbTopbarImageNameInput'); + if (topbarNameInput) topbarNameInput.value = newName; showToast(`Name updated`); } } catch (e) { @@ -9689,6 +9783,18 @@ } } + async function updateTopbarImageName() { + const fname = lbNames[lbIdx]; + const topbarNameInput = document.getElementById('lbTopbarImageNameInput'); + if (!topbarNameInput) return; + const newName = topbarNameInput.value; + + const nameInput = document.getElementById('lbNameInput'); + if (nameInput) nameInput.value = newName; + + await updateName(); + } + async function updateGroupName() { const gid = lbCurrentGid; if (!gid) return; @@ -11375,10 +11481,21 @@ async function _pollTurntableBadge() { try { let d = null; - try { - const r = await fetch(`${API}/output/_data/turntable_status.json?t=` + Date.now(), { signal: AbortSignal.timeout(2000) }); - if (r.ok) d = await r.json(); - } catch (_) {} + const gid = lbCurrentGid; + + if (gid) { + try { + const r = await fetch(`${API}/turntable/status/${encodeURIComponent(gid)}`); + if (r.ok) d = await r.json(); + } catch (_) {} + } + + if (!d) { + try { + const r = await fetch(`${API}/output/_data/turntable_status.json?t=` + Date.now(), { signal: AbortSignal.timeout(2000) }); + if (r.ok) d = await r.json(); + } catch (_) {} + } if (!d) { const r = await fetch(`${API}/turntable/status`); if (r.ok) d = await r.json(); @@ -11389,7 +11506,7 @@ const tbsDot = document.getElementById('tbsDot'); const tbsText = document.getElementById('tbsText'); const total = d.n_total || 0; - const done = d.n_complete || 0; + const done = (d.n_complete !== undefined ? d.n_complete : d.n_done) || 0; if (total === 0) { badge.style.display = 'none'; } else if (done < total) { badge.textContent = `${done}/${total}`; badge.style.display = ''; } else { badge.style.display = 'none'; } @@ -11413,8 +11530,15 @@ // Refresh orbit tab badge on the tab button const tabBtn = document.getElementById('sbTabOrbit'); - if (tabBtn && done > 0) { - tabBtn.textContent = `Orbit${done < total ? ' (' + done + '/' + total + ')' : ' βœ“'}`; + if (tabBtn) { + tabBtn.title = `Orbit 3D (${done}/${total})`; + if (total > 0 && done < total) { + tabBtn.innerHTML = `βš›${done}/${total}`; + } else if (total > 0 && done === total) { + tabBtn.innerHTML = `βš›βœ“`; + } else { + tabBtn.innerHTML = `βš›`; + } } } catch(_) {} } @@ -12155,7 +12279,8 @@ } document.getElementById('count').textContent = `${currentGroups.length} group${currentGroups.length !== 1 ? 's' : ''} Β· ${fileObjects.length} images`; - if (newFileNames.size > 0) showToast(`${newFileNames.size} new image${newFileNames.size !== 1 ? 's' : ''} detected`, 'success'); + if (newFileNames.size > 0 && !isInitialLoad) showToast(`${newFileNames.size} new image${newFileNames.size !== 1 ? 's' : ''} detected`, 'success'); + isInitialLoad = false; // If studio is open, sync lbUrls/lbNames from freshly-loaded groupData const studioEl = document.getElementById('studio'); @@ -12419,18 +12544,36 @@ _backendOnline = true; _modelEngineOnline = data.model_engine.online; + const backendClass = data.backend.busy ? 'status-dot-led busy' : 'status-dot-led online'; + const backendTitle = data.backend.busy ? 'Backend: Active task running' : 'Backend: Idle (Online)'; + + const modelClass = data.model_engine.online ? (data.model_engine.busy ? 'status-dot-led busy' : 'status-dot-led online') : 'status-dot-led offline'; + const modelTitle = data.model_engine.online + ? (data.model_engine.busy ? 'Model-Engine: Active inference running' : 'Model-Engine: Idle (Online)') + : 'Model-Engine: Offline (ComfyUI backend is down)'; + + // Dashboard dots const backendDot = document.getElementById('backendDot'); if (backendDot) { - backendDot.className = 'status-dot-led ' + (data.backend.busy ? 'busy' : 'online'); - backendDot.parentElement.title = data.backend.busy ? 'Backend: Active task running' : 'Backend: Idle (Online)'; + backendDot.className = backendClass; + backendDot.parentElement.title = backendTitle; } - const modelDot = document.getElementById('modelDot'); if (modelDot) { - modelDot.className = 'status-dot-led ' + (data.model_engine.online ? (data.model_engine.busy ? 'busy' : 'online') : 'offline'); - modelDot.parentElement.title = data.model_engine.online - ? (data.model_engine.busy ? 'Model-Engine: Active inference running' : 'Model-Engine: Idle (Online)') - : 'Model-Engine: Offline (ComfyUI backend is down)'; + modelDot.className = modelClass; + modelDot.parentElement.title = modelTitle; + } + + // Studio topbar dots + const studioBackendDot = document.getElementById('studioBackendDot'); + if (studioBackendDot) { + studioBackendDot.className = backendClass; + studioBackendDot.parentElement.title = backendTitle; + } + const studioModelDot = document.getElementById('studioModelDot'); + if (studioModelDot) { + studioModelDot.className = modelClass; + studioModelDot.parentElement.title = modelTitle; } } else { throw new Error("API responded with error"); @@ -12439,15 +12582,32 @@ _backendOnline = false; _modelEngineOnline = false; + const offlineClass = 'status-dot-led offline'; + const backendOfflineTitle = 'Backend: Offline (API server is down)'; + const modelOfflineTitle = 'Model-Engine: Status unknown (API server is down)'; + + // Dashboard dots const backendDot = document.getElementById('backendDot'); if (backendDot) { - backendDot.className = 'status-dot-led offline'; - backendDot.parentElement.title = 'Backend: Offline (API server is down)'; + backendDot.className = offlineClass; + backendDot.parentElement.title = backendOfflineTitle; } const modelDot = document.getElementById('modelDot'); if (modelDot) { - modelDot.className = 'status-dot-led offline'; - modelDot.parentElement.title = 'Model-Engine: Status unknown (API server is down)'; + modelDot.className = offlineClass; + modelDot.parentElement.title = modelOfflineTitle; + } + + // Studio topbar dots + const studioBackendDot = document.getElementById('studioBackendDot'); + if (studioBackendDot) { + studioBackendDot.className = offlineClass; + studioBackendDot.parentElement.title = backendOfflineTitle; + } + const studioModelDot = document.getElementById('studioModelDot'); + if (studioModelDot) { + studioModelDot.className = offlineClass; + studioModelDot.parentElement.title = modelOfflineTitle; } } @@ -13007,6 +13167,14 @@ document.title = PRIV_TITLE; showPrivacyOverlay(); } + + // Direct-to-dashboard back link for group specific pages + if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) { + const dashLink = document.getElementById('studioDashboardLink'); + const galBtn = document.getElementById('studioGalleryBtn'); + if (dashLink) dashLink.style.display = 'inline-flex'; + if (galBtn) galBtn.style.display = 'none'; + } }); async function toggleTagAction(tagName) { @@ -13095,7 +13263,7 @@ fileDescriptions[fname] = newDesc; showToast('Successfully updated both prompt and description!', 'success'); updateStudio(); - refreshNow(); + refreshNow(true); } else { showToast('Failed to update prompt or description', 'error'); } @@ -13120,17 +13288,19 @@ const el = document.getElementById(contentId); if (!el || el.querySelector('textarea')) return; + const buttonLabel = currentVal.trim() === '' ? '✨ Create' : '✨ Refine'; + el.onclick = null; el.innerHTML = ` - +
- +
`; - const ta = document.getElementById('editFieldArea'); + const ta = document.getElementById(`editFieldArea_${field}`); if (ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); @@ -13151,7 +13321,7 @@ const fname = lbNames[lbIdx]; if (!fname) return; - const ta = document.getElementById('editFieldArea'); + const ta = document.getElementById(`editFieldArea_${field}`); if (!ta) return; const newVal = ta.value.trim(); @@ -13174,7 +13344,7 @@ showToast('Character & scenery description updated successfully', 'success'); } updateStudio(); - refreshNow(); + refreshNow(true); } else { showToast('Failed to save update', 'error'); } @@ -13189,24 +13359,30 @@ const fname = lbNames[lbIdx]; if (!fname) return; - const ta = document.getElementById('editFieldArea'); + const ta = document.getElementById(`editFieldArea_${field}`); if (!ta) return; const currentVal = ta.value.trim(); - const instruction = prompt(`Enter instructions to refine this ${field}:`, "Make it more detailed..."); + const actionVerb = currentVal === '' ? 'create' : 'refine'; + const instruction = prompt(`Enter instructions to ${actionVerb} this ${field}:`, actionVerb === 'create' ? "Write a detailed description of..." : "Make it more detailed..."); if (!instruction) return; - showToast('AI is refining your text...', 'info'); + showToast(`AI is generating your ${field}...`, 'info'); try { const r = await fetch(`${API}/refine-prompt`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt: currentVal, user_instruction: instruction }) + body: JSON.stringify({ + prompt: currentVal, + user_instruction: instruction, + filename: fname, + field: field + }) }); if (r.ok) { const data = await r.json(); ta.value = data.refined; - showToast('Refined successfully! Click Save to apply.', 'success'); + showToast('Successfully updated! Click Save to apply.', 'success'); } else { showToast('Failed to refine field: ' + await r.text(), 'error'); } diff --git a/tour-comfy/database.py b/tour-comfy/database.py index 8642597..41e2c5f 100644 --- a/tour-comfy/database.py +++ b/tour-comfy/database.py @@ -422,7 +422,8 @@ def list_persons(include_archived=False): SELECT filename, name, group_id, clip_description, prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing, content_type, faceswap_source_video, archived, is_source, tags, - pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects, description + pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects, description, + filepath FROM person {where} """) diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py index 3680a93..7c70e15 100644 --- a/tour-comfy/edit_api.py +++ b/tour-comfy/edit_api.py @@ -1508,6 +1508,8 @@ def _write_all_static() -> None: "anatomical_completeness": p[20], "facial_direction": p[21], "objects": obj_list, + "description": p[23], + "filepath": p[24], }) print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived") try: @@ -2144,16 +2146,15 @@ def start_multi_ref(req: MultiRefRequest): class RefineRequest(BaseModel): - prompt: str + prompt: str = "" filename: str | None = None + user_instruction: str | None = None + field: str | None = None @app.post("/refine-prompt") def refine_prompt(req: RefineRequest): - """Refine a prompt using the external uncensored chat LLM.""" - if not req.prompt: - raise HTTPException(400, "Prompt is required") - + """Refine or create a prompt/description using the external uncensored chat LLM.""" context_str = "" if req.filename: try: @@ -2197,9 +2198,22 @@ def refine_prompt(req: RefineRequest): except Exception as db_err: print(f"[refine-prompt] database error for {req.filename}: {db_err}") - user_content = f"Refine this pose: {req.prompt}" + field_label = req.field if req.field else "text" + + if req.prompt: + if req.user_instruction: + user_content = f"Refine this {field_label} according to the following instructions:\n{req.user_instruction}\n\nOriginal {field_label}:\n{req.prompt}" + else: + user_content = f"Refine this {field_label} to make it more descriptive, detailed, and high-quality.\n\nOriginal {field_label}:\n{req.prompt}" + else: + # Prompt/text is empty, so we are creating one from scratch! + if req.user_instruction: + user_content = f"Create a new {field_label} from scratch according to these instructions:\n{req.user_instruction}" + else: + user_content = f"Create a new detailed and high-quality {field_label} from scratch." + if context_str: - user_content += f"\n\nUse the following image context details to ensure the refined prompt matches the reference characteristics closely:\n{context_str}" + user_content += f"\n\nUse the following image context details to ensure the output matches the reference characteristics closely:\n{context_str}" # Use the same API as gen_poses.py llm_api = "http://192.168.1.160:8001/v1/chat/completions" @@ -2221,7 +2235,8 @@ def refine_prompt(req: RefineRequest): try: database.save_db_prompt("refine", refined, { "original": req.prompt, - "filename": req.filename + "filename": req.filename, + "field": req.field }) except Exception as db_err: print(f"[refine-prompt] failed to save to prompt table: {db_err}") @@ -2712,6 +2727,7 @@ def list_images(archived: bool = False, bypass_static: bool = False): "facial_direction": p[21], "objects": obj_list, "description": p[23] if len(p) > 23 else None, + "filepath": p[24] if len(p) > 24 else None, }) db_images.sort( key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1], diff --git a/tour-comfy/poses.md b/tour-comfy/poses.md index d10786f..0524fc1 100644 --- a/tour-comfy/poses.md +++ b/tour-comfy/poses.md @@ -878,6 +878,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t # Lantern: +you are in a black void. kneeling, knees apart, sitting back on the heels. torso upright, spine long. arms extended overhead, hands clasped or holding an imaginary object. @@ -889,6 +890,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t # Compass: +you are in a black void. seated, one leg extended straight forward. other leg bent, foot placed on the inner thigh of the extended leg. torso folded forward over the extended leg. @@ -900,6 +902,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t # Vessel (beta): +you are in a black void. lying on back, knees bent, feet flat on the ground. arms extended overhead, hands clasped. hips lifted slightly, lower back arched. @@ -911,6 +914,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t # Quiver (beta): +you are in a black void. lying on stomach, legs together, arms at the sides. torso lifted in a backbend, chest and shoulders arching upward. head thrown back, neck stretched. @@ -922,6 +926,7 @@ Perfect anatomy, hyperrealistic # Loom: +you are in a black void. seated, legs extended straight forward, feet flexed. torso upright, spine long. arms extended forward, hands reaching toward the feet. @@ -3051,7 +3056,7 @@ You are standing in black void. A single solid steel device, polished and seamless. An internal plug, contoured and smooth, connected by a short rigid bar to a small external shield covering your vulva. From the shield, a thin rigid steel bar rises up your abdomen and sternum, terminating in a collar locked around your neck. -One continuous piece β€” internal plug, connecting bar, collar. +One continuous piece β€” internal plug, connecting bar, collar. Your wrists cuffed to the vertical bar at your hips. Your ankles cuffed together. The device occupies and seals simultaneously β€” internal and external denial in one form. @@ -4630,11 +4635,12 @@ sheen of perspiration, accentuating every muscle contour. Anatomically precise, # Integrated Collar Arms Restraint A subject kneels, your body bent into a powerful, inviting stance. -Your arms are restrained behind your back, firmly secured with an integral collar restraint, a sleek device that encircles your neck, connecting to your wrists with rigid steel bars. -Your hands are clasped together, adding to the restricted pose. Your knees are positioned under your hips, providing stability. -The back is arched slightly upward, head lifted, drawing attention to the exposed shoulders and back. +Your arms are restrained behind your back, firmly secured with an integral collar restraint, a sleek device that encircles your neck, connecting to your wrists with rigid steel bars. +Your hands are clasped together, adding to the restricted pose. Your knees are positioned under your hips, providing stability. +The back is arched slightly upward, head lifted, drawing attention to the exposed shoulders and back. Your gaze is locked onto the viewer, a look of defiance and anticipation. The pose conveys a mix of sultry energy and power, the steel bars creating an asymmetrical silhouette. Perfect anatomy, realistic. # nice pose + standing, feet apart, hands on the hips. torso twisted, one shoulder forward, one back. weight shifted to one leg, opposite hip popped. waist compressed, creating a pronounced hourglass silhouette. head turned to look over the shoulder. Looking back into camera. Pinched, exaggerated. Perfect anatomy, realistic \ No newline at end of file