updates UI
This commit is contained in:
@@ -1709,21 +1709,72 @@
|
|||||||
.sb-gen-refs .sb-gen-ref:hover .sb-gen-ref-x { display: flex; }
|
.sb-gen-refs .sb-gen-ref:hover .sb-gen-ref-x { display: flex; }
|
||||||
.sb-gen-refs .sb-gen-ref:hover img { border-color: #dc2626; }
|
.sb-gen-refs .sb-gen-ref:hover img { border-color: #dc2626; }
|
||||||
#studioAngleBar {
|
#studioAngleBar {
|
||||||
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
|
position: absolute;
|
||||||
display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s;
|
bottom: 12px;
|
||||||
z-index: 10; pointer-events: none;
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgba(10, 10, 10, 0.88);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.studio-viewer:hover #studioAngleBar {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
.studio-viewer:hover #studioAngleBar { opacity: 1; pointer-events: all; }
|
|
||||||
#studioAngleBar button {
|
#studioAngleBar button {
|
||||||
font-size: 15px; background: rgba(0,0,0,0.65); border: 1px solid rgba(255,255,255,0.18);
|
font-size: 13px;
|
||||||
border-radius: 4px; color: #fff; width: 34px; height: 34px; cursor: pointer; line-height: 1;
|
font-weight: 500;
|
||||||
transition: background 0.12s; position: relative;
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #ccc;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
#studioAngleBar button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
#studioAngleBar button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
#studioAngleBar button:hover { background: rgba(255,255,255,0.18); }
|
|
||||||
#studioAngleBar button[title]:hover::after {
|
#studioAngleBar button[title]:hover::after {
|
||||||
content: attr(title); position: absolute; bottom: 38px; left: 50%; transform: translateX(-50%);
|
content: attr(title);
|
||||||
background: rgba(0,0,0,0.8); color: #fff; font-size: 10px; padding: 2px 6px;
|
position: absolute;
|
||||||
border-radius: 3px; white-space: nowrap; pointer-events: none;
|
bottom: 42px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #333;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* template grid (faceswap / scenery) */
|
/* template grid (faceswap / scenery) */
|
||||||
@@ -1886,7 +1937,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Floating services status container -->
|
<!-- Floating services status container -->
|
||||||
<div class="services-status-container" style="position: fixed; top: 12px; right: 12px; z-index: 11000; display: flex; align-items: center; gap: 14px; background: rgba(18, 18, 18, 0.9); border: 1px solid #2e2e2e; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 11px; color: #aaa; box-shadow: 0 4px 12px rgba(0,0,0,0.5); backdrop-filter: blur(4px);">
|
<div class="services-status-container" style="position: fixed; bottom: 12px; left: 12px; z-index: 11000; display: flex; align-items: center; gap: 14px; background: rgba(18, 18, 18, 0.9); border: 1px solid #2e2e2e; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 11px; color: #aaa; box-shadow: 0 4px 12px rgba(0,0,0,0.5); backdrop-filter: blur(4px);">
|
||||||
<div style="display: flex; align-items: center; gap: 6px; cursor: help;" id="backendStatus" title="Backend: Offline (API server is down)">
|
<div style="display: flex; align-items: center; gap: 6px; cursor: help;" id="backendStatus" title="Backend: Offline (API server is down)">
|
||||||
<span class="status-dot-led offline" id="backendDot"></span>
|
<span class="status-dot-led offline" id="backendDot"></span>
|
||||||
<span>API</span>
|
<span>API</span>
|
||||||
@@ -2068,7 +2119,8 @@
|
|||||||
<span id="lbImgDims" style="font-size:11px;color:#444;margin:0 8px"></span>
|
<span id="lbImgDims" style="font-size:11px;color:#444;margin:0 8px"></span>
|
||||||
<span id="lbCounter" style="font-size:12px;color:#555"></span>
|
<span id="lbCounter" style="font-size:12px;color:#555"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="studio-topbar-right">
|
<div class="studio-topbar-right" style="display:flex;align-items:center;gap:6px">
|
||||||
|
<button class="btn" id="studioPrivacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)" style="font-size:12px;padding:4px 8px">🔓</button>
|
||||||
<button class="btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox">⊟</button>
|
<button class="btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox">⊟</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2144,13 +2196,23 @@
|
|||||||
<div class="sb-panels">
|
<div class="sb-panels">
|
||||||
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
|
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
|
||||||
<div class="sb-panel active" id="sbPanelInfo">
|
<div class="sb-panel active" id="sbPanelInfo">
|
||||||
<div id="lbGenPromptWrap" style="display:none;margin-bottom:8px">
|
<!-- Pose-directed Prompt -->
|
||||||
|
<div id="lbGenPromptWrap" style="display:none;margin-bottom:12px">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px">
|
||||||
<div class="sb-label" style="margin:0">Generation prompt</div>
|
<div class="sb-label" style="margin:0">Pose prompt (Actor-agnostic)</div>
|
||||||
<button class="sb-btn" onclick="suggestUpdatedDescription()" style="font-size:9px;padding:2px 6px;margin:0;min-width:auto;height:18px;line-height:1" title="Use AI to suggest an updated description from this image (Shortcut: R)">✨ Suggest Description</button>
|
<button class="sb-btn" onclick="suggestUpdatedDescription()" style="font-size:9px;padding:2px 6px;margin:0;min-width:auto;height:18px;line-height:1" title="Use AI to suggest updated pose prompt and character description (Shortcut: R)">✨ Suggest Details</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="lbGenPrompt" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word"></div>
|
<div id="lbGenPrompt" class="editable-meta-field" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word;background:#141414;border:1px solid #2a2a2a;padding:6px;border-radius:4px;cursor:pointer" onclick="editMetadataField('prompt')">Click to add/edit pose prompt...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Character & Scenery Description -->
|
||||||
|
<div id="lbDescriptionWrap" style="display:none;margin-bottom:12px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px">
|
||||||
|
<div class="sb-label" style="margin:0">Character & Scenery Description</div>
|
||||||
|
</div>
|
||||||
|
<div id="lbDescription" class="editable-meta-field" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word;background:#141414;border:1px solid #2a2a2a;padding:6px;border-radius:4px;cursor:pointer" onclick="editMetadataField('description')">Click to add/edit description...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="lbSourceRefsWrap" style="display:none;margin-bottom:8px">
|
<div id="lbSourceRefsWrap" style="display:none;margin-bottom:8px">
|
||||||
<div class="sb-label" style="margin-bottom:3px">Reference Images</div>
|
<div class="sb-label" style="margin-bottom:3px">Reference Images</div>
|
||||||
<div id="lbSourceRefs" class="sb-source-refs"></div>
|
<div id="lbSourceRefs" class="sb-source-refs"></div>
|
||||||
@@ -2231,32 +2293,51 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sb-sep"></div>
|
<div class="sb-sep"></div>
|
||||||
<div class="sb-label">Actions</div>
|
<div class="sb-label">Actions</div>
|
||||||
<div class="sb-actions" style="margin-bottom:8px">
|
|
||||||
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
|
<!-- Geometry & Retouching Tools -->
|
||||||
<button class="sb-btn purple" id="lbUndressBtn" style="display:none" onclick="lbUndress()">Undress</button>
|
<div style="font-size:10px;color:#666;margin-bottom:4px">Geometry & Retouching</div>
|
||||||
|
<div class="sb-actions" style="margin-bottom:8px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
|
||||||
|
<button class="sb-btn" id="lbPoseBtn" onclick="lbTogglePose()" title="Preview body pose skeleton" style="margin:0; width:100%">Pose</button>
|
||||||
|
<button class="sb-btn" id="lbInvertAlphaBtn" onclick="lbInvertAlpha()" title="Invert transparency — fixes when the wrong segment was kept" style="margin:0; width:100%">Invert α</button>
|
||||||
|
<button class="sb-btn" id="lbRotateLeftBtn" onclick="lbRotate(-90)" title="Rotate 90° counter-clockwise" style="margin:0; width:100%">⟲ Rotate L</button>
|
||||||
|
<button class="sb-btn" id="lbRotateRightBtn" onclick="lbRotate(90)" title="Rotate 90° clockwise" style="margin:0; width:100%">⟳ Rotate R</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sb-actions" style="margin-bottom:8px">
|
|
||||||
<button class="sb-btn" id="lbExtract" style="display:none" onclick="extractCurrentImage()">Extract</button>
|
<!-- AI & Generative features -->
|
||||||
<button class="sb-btn" id="lbInvertAlphaBtn" onclick="lbInvertAlpha()" title="Invert transparency — fixes when the wrong segment was kept">Invert α</button>
|
<div style="font-size:10px;color:#666;margin-bottom:4px">AI & Export</div>
|
||||||
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group">Duplicate</button>
|
<div class="sb-actions" style="margin-bottom:8px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
|
||||||
<button class="sb-btn" id="lbRotateLeftBtn" onclick="lbRotate(-90)" title="Rotate 90° counter-clockwise">⟲ 90°</button>
|
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger" style="margin:0; width:100%">Re-tag</button>
|
||||||
<button class="sb-btn" id="lbRotateRightBtn" onclick="lbRotate(90)" title="Rotate 90° clockwise">⟳ 90°</button>
|
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group" style="margin:0; width:100%">Duplicate</button>
|
||||||
<button class="sb-btn" id="lbPoseBtn" onclick="lbTogglePose()" title="Preview body pose skeleton">Pose</button>
|
<button class="sb-btn purple" id="lbUndressBtn" style="display:none; margin:0; width:100%" onclick="lbUndress()">Undress</button>
|
||||||
<button class="sb-btn" id="lbMakeVideoBtn" onclick="lbMakeVideo()" title="Stitch group images (or Shift+Click selection) into a short MP4">Make video</button>
|
<button class="sb-btn" id="lbExtract" style="display:none; margin:0; width:100%" onclick="extractCurrentImage()">Extract</button>
|
||||||
<button class="sb-btn amber" id="lbOrbitBtn" onclick="lbOrbit()" title="Generate orbit preview (Fast=depth-card, Quality=Qwen turntable)">Orbit 3D<span id="orbitBadge" class="orbit-badge" style="display:none"></span></button>
|
<button class="sb-btn" id="lbMakeVideoBtn" onclick="lbMakeVideo()" title="Stitch group images (or Shift+Click selection) into a short MP4" style="margin:0; width:100%">Make video</button>
|
||||||
<a class="sb-btn" id="lbDownloadBtn" download style="text-decoration:none">Download</a>
|
<button class="sb-btn amber" id="lbOrbitBtn" onclick="lbOrbit()" title="Generate orbit preview (Fast=depth-card, Quality=Qwen turntable)" style="margin:0; width:100%">Orbit 3D<span id="orbitBadge" class="orbit-badge" style="display:none"></span></button>
|
||||||
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b" title="Move to archive (recoverable)">Archive</button>
|
</div>
|
||||||
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" title="Click once to arm, then again to confirm">Delete</button>
|
|
||||||
|
<!-- File actions -->
|
||||||
|
<div style="font-size:10px;color:#666;margin-bottom:4px">File Management</div>
|
||||||
|
<div class="sb-actions" style="margin-bottom:8px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px;">
|
||||||
|
<a class="sb-btn" id="lbDownloadBtn" download style="text-decoration:none; margin:0; width:100%; text-align:center; padding: 4px 0">Download</a>
|
||||||
|
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b; margin:0; width:100%" title="Move to archive (recoverable)">Archive</button>
|
||||||
|
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" style="margin:0; width:100%" title="Click once to arm, then again to confirm">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sb-sep"></div>
|
<div class="sb-sep"></div>
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||||||
<div class="sb-label" style="margin:0">Image name</div>
|
<div class="sb-label" style="margin:0">Image name</div>
|
||||||
<button class="sb-btn" onclick="copyRawFilename()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy raw filename to clipboard">
|
<div style="display:flex;gap:4px">
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
|
<button class="sb-btn" onclick="copyRawFilename()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy raw filename to clipboard">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
</svg>Copy
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
</button>
|
</svg>Copy ID
|
||||||
|
</button>
|
||||||
|
<button class="sb-btn" onclick="copyFilepath()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy absolute file path to clipboard">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>Copy Path
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="sb-input" id="lbNameInput"
|
<input type="text" class="sb-input" id="lbNameInput"
|
||||||
placeholder="Image name…"
|
placeholder="Image name…"
|
||||||
@@ -2390,6 +2471,7 @@
|
|||||||
<button class="btn" onclick="selectAll()">All</button>
|
<button class="btn" onclick="selectAll()">All</button>
|
||||||
<button class="btn" onclick="deselectAll()">None</button>
|
<button class="btn" onclick="deselectAll()">None</button>
|
||||||
<button class="btn" id="groupBtn" onclick="groupSelected()">Group</button>
|
<button class="btn" id="groupBtn" onclick="groupSelected()">Group</button>
|
||||||
|
<button class="btn" id="openStudioBtn" onclick="openStudioForSelected()" style="display:none">Open Studio</button>
|
||||||
<button class="btn" id="multiRefBtn" onclick="processMultiRef()" style="display:none" title="Use selected (2–3) as combined references → single output">Multi-ref</button>
|
<button class="btn" id="multiRefBtn" onclick="processMultiRef()" style="display:none" title="Use selected (2–3) as combined references → single output">Multi-ref</button>
|
||||||
<span id="multiRefLegend" style="display:none;font-size:10px;color:#888;white-space:nowrap"></span>
|
<span id="multiRefLegend" style="display:none;font-size:10px;color:#888;white-space:nowrap"></span>
|
||||||
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
|
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
|
||||||
@@ -2421,6 +2503,11 @@
|
|||||||
|
|
||||||
// --- HYDRATION_START ---
|
// --- HYDRATION_START ---
|
||||||
const PRELOADED_IMAGES = [
|
const PRELOADED_IMAGES = [
|
||||||
|
"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",
|
||||||
|
"20260630_132351_sc_2_20260622_101128_image.nobg.nobg.png",
|
||||||
|
"20260618_052559_2_20260618_052526_image.nobg.png",
|
||||||
"20260701_031014_4_20260618_052526_image.png",
|
"20260701_031014_4_20260618_052526_image.png",
|
||||||
"20260701_030956_4_20260618_052526_image.png",
|
"20260701_030956_4_20260618_052526_image.png",
|
||||||
"20260701_030939_4_20260618_052526_image.png",
|
"20260701_030939_4_20260618_052526_image.png",
|
||||||
@@ -2513,7 +2600,6 @@
|
|||||||
"20260630_232932_2_20260618_052526_image.nobg.png",
|
"20260630_232932_2_20260618_052526_image.nobg.png",
|
||||||
"20260630_232800_2_20260618_052526_image.nobg.png",
|
"20260630_232800_2_20260618_052526_image.nobg.png",
|
||||||
"20260630_232800_2_20260618_052526_image.png",
|
"20260630_232800_2_20260618_052526_image.png",
|
||||||
"20260618_052559_2_20260618_052526_image.nobg.png",
|
|
||||||
"20260630_231721_sc_5_20260618_052526_image.png",
|
"20260630_231721_sc_5_20260618_052526_image.png",
|
||||||
"20260630_163400_image.png",
|
"20260630_163400_image.png",
|
||||||
"20260630_163247_3_20260628_205049_image.png",
|
"20260630_163247_3_20260628_205049_image.png",
|
||||||
@@ -2523,13 +2609,9 @@
|
|||||||
"20260630_151554_sc_0_20260622_101128_image.png",
|
"20260630_151554_sc_0_20260622_101128_image.png",
|
||||||
"20260630_151502_sc_0_20260622_101128_image.png",
|
"20260630_151502_sc_0_20260622_101128_image.png",
|
||||||
"20260630_132351_sc_2_20260622_101128_image.nobg.png",
|
"20260630_132351_sc_2_20260622_101128_image.nobg.png",
|
||||||
"20260630_132133_sc_2_20260622_101128_image.nobg.png",
|
|
||||||
"20260630_131910_sc_2_20260622_101128_image.nobg.png",
|
"20260630_131910_sc_2_20260622_101128_image.nobg.png",
|
||||||
"20260630_131638_sc_2_20260622_101128_image.nobg.png",
|
|
||||||
"20260630_131402_sc_2_20260622_101128_image.nobg.png",
|
"20260630_131402_sc_2_20260622_101128_image.nobg.png",
|
||||||
"20260630_130738_sc_2_20260622_101128_image.nobg.png",
|
"20260630_130738_sc_2_20260622_101128_image.nobg.png",
|
||||||
"20260630_130601_sc_4_20260622_101128_image.png",
|
|
||||||
"20260630_130454_sc_0_20260622_101128_image.png",
|
|
||||||
"20260630_130350_sc_0_20260622_101128_image.png",
|
"20260630_130350_sc_0_20260622_101128_image.png",
|
||||||
"20260630_130220_sc_0_20260622_101128_image.png",
|
"20260630_130220_sc_0_20260622_101128_image.png",
|
||||||
"20260630_125808_sc_0_20260622_101128_image.png",
|
"20260630_125808_sc_0_20260622_101128_image.png",
|
||||||
@@ -2913,7 +2995,6 @@
|
|||||||
"20260629_060736_2_20260629_060652_image.png",
|
"20260629_060736_2_20260629_060652_image.png",
|
||||||
"20260629_060725_1_20260629_060652_image.png",
|
"20260629_060725_1_20260629_060652_image.png",
|
||||||
"20260629_060714_0_20260629_060652_image.png",
|
"20260629_060714_0_20260629_060652_image.png",
|
||||||
"20260629_060652_image.png",
|
|
||||||
"cg_fb1043d8_face.png",
|
"cg_fb1043d8_face.png",
|
||||||
"20260629_055242_image.png",
|
"20260629_055242_image.png",
|
||||||
"_turntable/cg_2bbb563a/views/view_022_330deg.png",
|
"_turntable/cg_2bbb563a/views/view_022_330deg.png",
|
||||||
@@ -5832,10 +5913,6 @@
|
|||||||
"20260626_005504_Pasted image (9).png",
|
"20260626_005504_Pasted image (9).png",
|
||||||
"20260626_005322_dup_20260626_003125_dup_Pasted image (9).png",
|
"20260626_005322_dup_20260626_003125_dup_Pasted image (9).png",
|
||||||
"20260626_003125_dup_Pasted image (9).png",
|
"20260626_003125_dup_Pasted image (9).png",
|
||||||
"20260625_232346_sc_image.png",
|
|
||||||
"20260625_232242_sc_image.png",
|
|
||||||
"20260625_232204_sc_image.png",
|
|
||||||
"20260625_231601_sc_image.png",
|
|
||||||
"20260625_225929_sc_image.png",
|
"20260625_225929_sc_image.png",
|
||||||
"20260625_223304_p12.png",
|
"20260625_223304_p12.png",
|
||||||
"20260625_210437_image.png",
|
"20260625_210437_image.png",
|
||||||
@@ -7876,12 +7953,14 @@
|
|||||||
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
|
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
|
||||||
let _prevGid = null; // for "Back" button in studio
|
let _prevGid = null; // for "Back" button in studio
|
||||||
let imageNames = {}; // filename → human name (from /names)
|
let imageNames = {}; // filename → human name (from /names)
|
||||||
|
let fileFilepaths = {}; // filename → absolute path on disk
|
||||||
let clipDescriptions = {}; // filename → description
|
let clipDescriptions = {}; // filename → description
|
||||||
let customGroups = {}; // filename → groupId (from /groups)
|
let customGroups = {}; // filename → groupId (from /groups)
|
||||||
let groupNames = {}; // group_id → group display name
|
let groupNames = {}; // group_id → group display name
|
||||||
let fileSortOrders = {}; // filename → sort_order (integer or null)
|
let fileSortOrders = {}; // filename → sort_order (integer or null)
|
||||||
let filePoses = {}; // filename → pose name
|
let filePoses = {}; // filename → pose name
|
||||||
let filePrompts = {}; // filename → generation prompt
|
let filePrompts = {}; // filename → generation prompt
|
||||||
|
let fileDescriptions = {}; // filename → custom description
|
||||||
let fileHidden = {}; // filename → boolean (hidden from cycling)
|
let fileHidden = {}; // filename → boolean (hidden from cycling)
|
||||||
let fileArchived = {}; // filename → boolean
|
let fileArchived = {}; // filename → boolean
|
||||||
let filePoseDescription = {}; // filename → human readable string
|
let filePoseDescription = {}; // filename → human readable string
|
||||||
@@ -8911,6 +8990,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeStudio() {
|
function closeStudio() {
|
||||||
|
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||||
|
window.location.href = 'car.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById('studio').classList.remove('open');
|
document.getElementById('studio').classList.remove('open');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
stopSlideshow();
|
stopSlideshow();
|
||||||
@@ -9176,11 +9259,34 @@
|
|||||||
const clipEl = document.getElementById('lbClipDesc');
|
const clipEl = document.getElementById('lbClipDesc');
|
||||||
if (clipEl) clipEl.textContent = clipDescriptions[fname] || '';
|
if (clipEl) clipEl.textContent = clipDescriptions[fname] || '';
|
||||||
|
|
||||||
|
// Always show prompt and description wraps if fname exists, so user can edit/create even if empty
|
||||||
const genPromptWrap = document.getElementById('lbGenPromptWrap');
|
const genPromptWrap = document.getElementById('lbGenPromptWrap');
|
||||||
const genPromptEl = document.getElementById('lbGenPrompt');
|
const genPromptEl = document.getElementById('lbGenPrompt');
|
||||||
const genPrompt = filePrompts[fname] || '';
|
if (genPromptWrap && genPromptEl) {
|
||||||
if (genPromptWrap) genPromptWrap.style.display = genPrompt ? '' : 'none';
|
genPromptWrap.style.display = fname ? '' : 'none';
|
||||||
if (genPromptEl) genPromptEl.textContent = genPrompt;
|
const promptVal = filePrompts[fname] || '';
|
||||||
|
// Don't overwrite if actively editing (has textarea)
|
||||||
|
if (!genPromptEl.querySelector('textarea')) {
|
||||||
|
genPromptEl.textContent = promptVal || 'Click to add/edit pose prompt...';
|
||||||
|
genPromptEl.style.fontStyle = promptVal ? '' : 'italic';
|
||||||
|
genPromptEl.style.color = promptVal ? '' : '#555';
|
||||||
|
genPromptEl.onclick = () => editMetadataField('prompt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const descWrap = document.getElementById('lbDescriptionWrap');
|
||||||
|
const descEl = document.getElementById('lbDescription');
|
||||||
|
if (descWrap && descEl) {
|
||||||
|
descWrap.style.display = fname ? '' : 'none';
|
||||||
|
const descVal = fileDescriptions[fname] || '';
|
||||||
|
// Don't overwrite if actively editing (has textarea)
|
||||||
|
if (!descEl.querySelector('textarea')) {
|
||||||
|
descEl.textContent = descVal || 'Click to add/edit description...';
|
||||||
|
descEl.style.fontStyle = descVal ? '' : 'italic';
|
||||||
|
descEl.style.color = descVal ? '' : '#555';
|
||||||
|
descEl.onclick = () => editMetadataField('description');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pose = filePoses[fname];
|
const pose = filePoses[fname];
|
||||||
const poseTag = document.getElementById('lbPoseTag');
|
const poseTag = document.getElementById('lbPoseTag');
|
||||||
@@ -10874,6 +10980,21 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyFilepath() {
|
||||||
|
const fname = lbNames[lbIdx];
|
||||||
|
if (!fname) return;
|
||||||
|
const filepath = fileFilepaths[fname] || '';
|
||||||
|
if (!filepath) {
|
||||||
|
showToast('File path not available', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(filepath).then(() => {
|
||||||
|
showToast('Absolute file path copied to clipboard', 'success');
|
||||||
|
}).catch(err => {
|
||||||
|
showToast('Failed to copy: ' + err, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function lbRemoveBg() {
|
async function lbRemoveBg() {
|
||||||
if (!_backendOnline) {
|
if (!_backendOnline) {
|
||||||
showToast("Cannot remove background: Backend API is offline", "error");
|
showToast("Cannot remove background: Backend API is offline", "error");
|
||||||
@@ -11350,6 +11471,13 @@
|
|||||||
if (!r.ok) { panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>'; return; }
|
if (!r.ok) { panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>'; return; }
|
||||||
const { turntables } = await r.json();
|
const { turntables } = await r.json();
|
||||||
_turntablesData = turntables || [];
|
_turntablesData = turntables || [];
|
||||||
|
|
||||||
|
// Filter by LOAD_ONLY_GROUP_ID or window._selectedOrbitGroupIds
|
||||||
|
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||||
|
_turntablesData = _turntablesData.filter(t => t.group_id === LOAD_ONLY_GROUP_ID);
|
||||||
|
} else if (window._selectedOrbitGroupIds && window._selectedOrbitGroupIds.length > 0) {
|
||||||
|
_turntablesData = _turntablesData.filter(t => window._selectedOrbitGroupIds.includes(t.group_id));
|
||||||
|
}
|
||||||
} catch(_) {
|
} catch(_) {
|
||||||
panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>';
|
panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>';
|
||||||
return;
|
return;
|
||||||
@@ -11689,6 +11817,8 @@
|
|||||||
if (img.clip_description)clipDescriptions[img.filename]= img.clip_description;
|
if (img.clip_description)clipDescriptions[img.filename]= img.clip_description;
|
||||||
if (img.pose) filePoses[img.filename] = img.pose;
|
if (img.pose) filePoses[img.filename] = img.pose;
|
||||||
if (img.prompt) filePrompts[img.filename] = img.prompt;
|
if (img.prompt) filePrompts[img.filename] = img.prompt;
|
||||||
|
if (img.description) fileDescriptions[img.filename] = img.description;
|
||||||
|
if (img.filepath) fileFilepaths[img.filename] = img.filepath;
|
||||||
if (img.group_name && img.group_id) groupNames[img.group_id] = img.group_name;
|
if (img.group_name && img.group_id) groupNames[img.group_id] = img.group_name;
|
||||||
fileSortOrders[img.filename] = img.sort_order ?? null;
|
fileSortOrders[img.filename] = img.sort_order ?? null;
|
||||||
fileHidden[img.filename] = !!img.hidden;
|
fileHidden[img.filename] = !!img.hidden;
|
||||||
@@ -11732,7 +11862,8 @@
|
|||||||
if (bypassStatic) throw new Error("Bypass static requested");
|
if (bypassStatic) throw new Error("Bypass static requested");
|
||||||
let jsonUrl = `${API}/output/_data/images.json?t=${Date.now()}`;
|
let jsonUrl = `${API}/output/_data/images.json?t=${Date.now()}`;
|
||||||
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||||
jsonUrl = `${API}/output/_data/group_${LOAD_ONLY_GROUP_ID}.json?t=${Date.now()}`;
|
const sanitizedGid = LOAD_ONLY_GROUP_ID.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
||||||
|
jsonUrl = `${API}/output/_data/group_${sanitizedGid}.json?t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
const r = await fetch(jsonUrl, { signal: AbortSignal.timeout(4000) });
|
const r = await fetch(jsonUrl, { signal: AbortSignal.timeout(4000) });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
@@ -12319,6 +12450,19 @@
|
|||||||
modelDot.parentElement.title = 'Model-Engine: Status unknown (API server is down)';
|
modelDot.parentElement.title = 'Model-Engine: Status unknown (API server is down)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply visibility adjustments based on backend state
|
||||||
|
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||||
|
// Specific group page: hide/show hamburger and studio angle bar toolbar
|
||||||
|
const sidebarToggle = document.getElementById('sidebarInnerToggle');
|
||||||
|
if (sidebarToggle) sidebarToggle.style.display = _backendOnline ? '' : 'none';
|
||||||
|
const angleBar = document.getElementById('studioAngleBar');
|
||||||
|
if (angleBar) angleBar.style.display = _backendOnline ? '' : 'none';
|
||||||
|
} else {
|
||||||
|
// Landing page: hide/show upload field
|
||||||
|
const uploadZone = document.querySelector('.upload-zone');
|
||||||
|
if (uploadZone) uploadZone.style.display = _backendOnline ? '' : 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start polling services status every 3 seconds
|
// Start polling services status every 3 seconds
|
||||||
@@ -12467,7 +12611,12 @@
|
|||||||
toggleFile(gid);
|
toggleFile(gid);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
openLightbox(gid);
|
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||||
|
openLightbox(gid);
|
||||||
|
} else {
|
||||||
|
const sanitizedGid = gid.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
||||||
|
window.location.href = `shoot_${sanitizedGid}.html`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12504,6 +12653,11 @@
|
|||||||
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
|
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
|
||||||
const showMR = (n >= 2 && n <= 3);
|
const showMR = (n >= 2 && n <= 3);
|
||||||
document.getElementById('multiRefBtn').style.display = showMR ? '' : 'none';
|
document.getElementById('multiRefBtn').style.display = showMR ? '' : 'none';
|
||||||
|
const openStudioBtn = document.getElementById('openStudioBtn');
|
||||||
|
if (openStudioBtn) {
|
||||||
|
openStudioBtn.style.display = (n >= 1) ? '' : 'none';
|
||||||
|
openStudioBtn.textContent = `Open Studio (${n})`;
|
||||||
|
}
|
||||||
const legend = document.getElementById('multiRefLegend');
|
const legend = document.getElementById('multiRefLegend');
|
||||||
if (legend) {
|
if (legend) {
|
||||||
if (showMR) {
|
if (showMR) {
|
||||||
@@ -12521,6 +12675,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openStudioForSelected() {
|
||||||
|
if (selectedFiles.size === 0) {
|
||||||
|
showToast("Please select at least one group first", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window._selectedOrbitGroupIds = Array.from(selectedFiles);
|
||||||
|
const firstGid = window._selectedOrbitGroupIds[0];
|
||||||
|
|
||||||
|
// Turn off selection mode so the studio behaves normally
|
||||||
|
toggleSelectMode();
|
||||||
|
|
||||||
|
openStudio(firstGid, 0);
|
||||||
|
|
||||||
|
// Switch to the orbit tab automatically
|
||||||
|
switchSidebarTab('orbit');
|
||||||
|
}
|
||||||
|
|
||||||
function selectAll() {
|
function selectAll() {
|
||||||
currentGroups.forEach(g => selectedFiles.add(g.gid));
|
currentGroups.forEach(g => selectedFiles.add(g.gid));
|
||||||
updateCardSelection();
|
updateCardSelection();
|
||||||
@@ -12827,6 +12998,12 @@
|
|||||||
const pBtn = document.getElementById('privacyBtn');
|
const pBtn = document.getElementById('privacyBtn');
|
||||||
if (privacyMode && pBtn) {
|
if (privacyMode && pBtn) {
|
||||||
pBtn.textContent = '🔒';
|
pBtn.textContent = '🔒';
|
||||||
|
}
|
||||||
|
const sBtn = document.getElementById('studioPrivacyBtn');
|
||||||
|
if (privacyMode && sBtn) {
|
||||||
|
sBtn.textContent = '🔒';
|
||||||
|
}
|
||||||
|
if (privacyMode) {
|
||||||
document.title = PRIV_TITLE;
|
document.title = PRIV_TITLE;
|
||||||
showPrivacyOverlay();
|
showPrivacyOverlay();
|
||||||
}
|
}
|
||||||
@@ -12892,7 +13069,7 @@
|
|||||||
async function suggestUpdatedDescription() {
|
async function suggestUpdatedDescription() {
|
||||||
const fname = lbNames[lbIdx];
|
const fname = lbNames[lbIdx];
|
||||||
if (!fname) return;
|
if (!fname) return;
|
||||||
showToast('AI is analyzing the image and suggesting a description...', 'info');
|
showToast('AI is analyzing the image and suggesting description/prompt...', 'info');
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/reverse-engineer`, {
|
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/reverse-engineer`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
@@ -12900,23 +13077,31 @@
|
|||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
const newPrompt = data.prompt;
|
const newPrompt = data.prompt;
|
||||||
if (confirm(`Suggested Reconstructed Prompt:\n\n"${newPrompt}"\n\nWould you like to set this as the image generation prompt?`)) {
|
const newDesc = data.description;
|
||||||
const updateResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-prompt`, {
|
|
||||||
|
if (confirm(`Suggested Pose-Directed Prompt:\n"${newPrompt}"\n\nSuggested Character & Scenery Description:\n"${newDesc}"\n\nWould you like to set both?`)) {
|
||||||
|
const pResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-prompt`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ prompt: newPrompt })
|
body: JSON.stringify({ prompt: newPrompt })
|
||||||
});
|
});
|
||||||
if (updateResp.ok) {
|
const dResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-description`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ description: newDesc })
|
||||||
|
});
|
||||||
|
if (pResp.ok && dResp.ok) {
|
||||||
filePrompts[fname] = newPrompt;
|
filePrompts[fname] = newPrompt;
|
||||||
showToast('Successfully updated image prompt!', 'success');
|
fileDescriptions[fname] = newDesc;
|
||||||
|
showToast('Successfully updated both prompt and description!', 'success');
|
||||||
updateStudio();
|
updateStudio();
|
||||||
refreshNow();
|
refreshNow();
|
||||||
} else {
|
} else {
|
||||||
showToast('Failed to update image prompt', 'error');
|
showToast('Failed to update prompt or description', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('Failed to generate suggest description: ' + await r.text(), 'error');
|
showToast('Failed to generate suggestions: ' + await r.text(), 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -12924,6 +13109,113 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editMetadataField(field) {
|
||||||
|
const fname = lbNames[lbIdx];
|
||||||
|
if (!fname) return;
|
||||||
|
|
||||||
|
const isPrompt = field === 'prompt';
|
||||||
|
const contentId = isPrompt ? 'lbGenPrompt' : 'lbDescription';
|
||||||
|
|
||||||
|
const currentVal = isPrompt ? (filePrompts[fname] || '') : (fileDescriptions[fname] || '');
|
||||||
|
const el = document.getElementById(contentId);
|
||||||
|
if (!el || el.querySelector('textarea')) return;
|
||||||
|
|
||||||
|
el.onclick = null;
|
||||||
|
el.innerHTML = `
|
||||||
|
<textarea id="editFieldArea" style="width:100%;min-height:80px;background:#0d0d0d;border:1px solid #333;color:#ccc;font-size:11px;font-family:monospace;padding:6px;border-radius:4px;outline:none;resize:vertical;line-height:1.4;margin-bottom:4px">${currentVal}</textarea>
|
||||||
|
<div style="display:flex;gap:6px;justify-content:flex-end">
|
||||||
|
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="cancelEditField(event, '${field}')">Cancel</button>
|
||||||
|
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="refineField(event, '${field}')">✨ Refine</button>
|
||||||
|
<button class="sb-btn" style="padding:2px 8px;font-size:10px;background:#10b981;color:#fff" onclick="saveEditField(event, '${field}')">Save</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ta = document.getElementById('editFieldArea');
|
||||||
|
if (ta) {
|
||||||
|
ta.focus();
|
||||||
|
ta.setSelectionRange(ta.value.length, ta.value.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditField(event, field) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
const isPrompt = field === 'prompt';
|
||||||
|
const contentId = isPrompt ? 'lbGenPrompt' : 'lbDescription';
|
||||||
|
const el = document.getElementById(contentId);
|
||||||
|
if (el) el.innerHTML = '';
|
||||||
|
updateStudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditField(event, field) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
const fname = lbNames[lbIdx];
|
||||||
|
if (!fname) return;
|
||||||
|
|
||||||
|
const ta = document.getElementById('editFieldArea');
|
||||||
|
if (!ta) return;
|
||||||
|
const newVal = ta.value.trim();
|
||||||
|
|
||||||
|
const isPrompt = field === 'prompt';
|
||||||
|
const endpoint = isPrompt ? 'update-prompt' : 'update-description';
|
||||||
|
const bodyKey = isPrompt ? 'prompt' : 'description';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/${endpoint}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ [bodyKey]: newVal })
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
if (isPrompt) {
|
||||||
|
filePrompts[fname] = newVal;
|
||||||
|
showToast('Pose prompt updated successfully', 'success');
|
||||||
|
} else {
|
||||||
|
fileDescriptions[fname] = newVal;
|
||||||
|
showToast('Character & scenery description updated successfully', 'success');
|
||||||
|
}
|
||||||
|
updateStudio();
|
||||||
|
refreshNow();
|
||||||
|
} else {
|
||||||
|
showToast('Failed to save update', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showToast('Error saving update: ' + err, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refineField(event, field) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
const fname = lbNames[lbIdx];
|
||||||
|
if (!fname) return;
|
||||||
|
|
||||||
|
const ta = document.getElementById('editFieldArea');
|
||||||
|
if (!ta) return;
|
||||||
|
const currentVal = ta.value.trim();
|
||||||
|
|
||||||
|
const instruction = prompt(`Enter instructions to refine this ${field}:`, "Make it more detailed...");
|
||||||
|
if (!instruction) return;
|
||||||
|
|
||||||
|
showToast('AI is refining your text...', 'info');
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${API}/refine-prompt`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ prompt: currentVal, user_instruction: instruction })
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
const data = await r.json();
|
||||||
|
ta.value = data.refined;
|
||||||
|
showToast('Refined successfully! Click Save to apply.', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Failed to refine field: ' + await r.text(), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showToast('Error refining: ' + err, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updatePoseRatioSuggestion() {
|
function updatePoseRatioSuggestion() {
|
||||||
const st = window._poseState;
|
const st = window._poseState;
|
||||||
const container = document.getElementById('poseRatioSuggestion');
|
const container = document.getElementById('poseRatioSuggestion');
|
||||||
@@ -13167,6 +13459,26 @@
|
|||||||
window.addEventListener('blur', () => { if (privacyMode) showPrivacyOverlay(); });
|
window.addEventListener('blur', () => { if (privacyMode) showPrivacyOverlay(); });
|
||||||
window.addEventListener('pagehide', () => { if (privacyMode) showPrivacyOverlay(); });
|
window.addEventListener('pagehide', () => { if (privacyMode) showPrivacyOverlay(); });
|
||||||
|
|
||||||
|
window.addEventListener('storage', (e) => {
|
||||||
|
if (e.key === 'privacyMode') {
|
||||||
|
const newPrivacy = e.newValue !== 'false';
|
||||||
|
if (newPrivacy !== privacyMode) {
|
||||||
|
privacyMode = newPrivacy;
|
||||||
|
const btn = document.getElementById('privacyBtn');
|
||||||
|
if (btn) btn.textContent = privacyMode ? '🔒' : '🔓';
|
||||||
|
const sBtn = document.getElementById('studioPrivacyBtn');
|
||||||
|
if (sBtn) sBtn.textContent = privacyMode ? '🔒' : '🔓';
|
||||||
|
if (privacyMode) {
|
||||||
|
document.title = PRIV_TITLE;
|
||||||
|
showPrivacyOverlay();
|
||||||
|
} else {
|
||||||
|
document.title = REAL_TITLE;
|
||||||
|
document.getElementById('privacyOverlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
if (privacyMode) showPrivacyOverlay();
|
if (privacyMode) showPrivacyOverlay();
|
||||||
@@ -13180,9 +13492,12 @@
|
|||||||
privacyMode = !privacyMode;
|
privacyMode = !privacyMode;
|
||||||
localStorage.setItem('privacyMode', privacyMode);
|
localStorage.setItem('privacyMode', privacyMode);
|
||||||
const btn = document.getElementById('privacyBtn');
|
const btn = document.getElementById('privacyBtn');
|
||||||
btn.textContent = privacyMode ? '🔒' : '🔓';
|
if (btn) btn.textContent = privacyMode ? '🔒' : '🔓';
|
||||||
|
const sBtn = document.getElementById('studioPrivacyBtn');
|
||||||
|
if (sBtn) sBtn.textContent = privacyMode ? '🔒' : '🔓';
|
||||||
if (privacyMode) {
|
if (privacyMode) {
|
||||||
document.title = PRIV_TITLE; // keep the taskbar label discreet even while active
|
document.title = PRIV_TITLE; // keep the taskbar label discreet even while active
|
||||||
|
showPrivacyOverlay();
|
||||||
} else {
|
} else {
|
||||||
document.title = REAL_TITLE;
|
document.title = REAL_TITLE;
|
||||||
document.getElementById('privacyOverlay').style.display = 'none';
|
document.getElementById('privacyOverlay').style.display = 'none';
|
||||||
@@ -13752,6 +14067,7 @@
|
|||||||
const nSafe = a.name.replace(/'/g, "\\'");
|
const nSafe = a.name.replace(/'/g, "\\'");
|
||||||
return `<button onclick="submitSingleAngle('${pSafe}', '${nSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
|
return `<button onclick="submitSingleAngle('${pSafe}', '${nSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
|
||||||
}).join('') + `
|
}).join('') + `
|
||||||
|
<div style="width: 1px; height: 18px; background: rgba(255,255,255,0.15); margin: 0 6px; flex-shrink: 0;"></div>
|
||||||
<button onclick="startManualPad()" title="Pad canvas (Expand image)" style="color:#f59e0b">
|
<button onclick="startManualPad()" title="Pad canvas (Expand image)" style="color:#f59e0b">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
|
||||||
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
|
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ def migrate_schema():
|
|||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS anatomical_completeness BOOLEAN DEFAULT NULL",
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS anatomical_completeness BOOLEAN DEFAULT NULL",
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS facial_direction TEXT DEFAULT NULL",
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS facial_direction TEXT DEFAULT NULL",
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS objects JSONB DEFAULT NULL",
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS objects JSONB DEFAULT NULL",
|
||||||
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS description TEXT",
|
||||||
]:
|
]:
|
||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -256,7 +257,27 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
|||||||
content_type=None, faceswap_source_video=None, archived=None,
|
content_type=None, faceswap_source_video=None, archived=None,
|
||||||
face_embedding=None, is_source=None,
|
face_embedding=None, is_source=None,
|
||||||
pose_description=None, pose_skeleton=None,
|
pose_description=None, pose_skeleton=None,
|
||||||
people_count=None, anatomical_completeness=None, facial_direction=None, objects=None):
|
people_count=None, anatomical_completeness=None, facial_direction=None, objects=None,
|
||||||
|
description=None):
|
||||||
|
if tags is not None:
|
||||||
|
if isinstance(tags, str):
|
||||||
|
try:
|
||||||
|
tags_list = json.loads(tags)
|
||||||
|
except Exception:
|
||||||
|
tags_list = []
|
||||||
|
elif isinstance(tags, list):
|
||||||
|
tags_list = tags
|
||||||
|
else:
|
||||||
|
tags_list = []
|
||||||
|
|
||||||
|
# Enforce that ORBIT is only on _turntable/ files
|
||||||
|
if filename.startswith("_turntable/"):
|
||||||
|
if "ORBIT" not in tags_list:
|
||||||
|
tags_list.append("ORBIT")
|
||||||
|
else:
|
||||||
|
tags_list = [t for t in tags_list if t != "ORBIT"]
|
||||||
|
tags = tags_list
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
face_embedding_str = ("[" + ",".join(map(str, face_embedding)) + "]") if face_embedding is not None else None
|
face_embedding_str = ("[" + ",".join(map(str, face_embedding)) + "]") if face_embedding is not None else None
|
||||||
@@ -266,8 +287,9 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
|||||||
clip_description, prompt, pose, sort_order, group_name, hidden,
|
clip_description, prompt, pose, sort_order, group_name, hidden,
|
||||||
has_background, source_refs, has_clothing,
|
has_background, source_refs, has_clothing,
|
||||||
content_type, faceswap_source_video, archived, face_embedding, is_source,
|
content_type, faceswap_source_video, archived, face_embedding, is_source,
|
||||||
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects)
|
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
description)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON CONFLICT (filename) DO UPDATE
|
ON CONFLICT (filename) DO UPDATE
|
||||||
SET filepath = COALESCE(EXCLUDED.filepath, person.filepath),
|
SET filepath = COALESCE(EXCLUDED.filepath, person.filepath),
|
||||||
name = COALESCE(EXCLUDED.name, person.name),
|
name = COALESCE(EXCLUDED.name, person.name),
|
||||||
@@ -293,14 +315,16 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
|||||||
people_count = COALESCE(EXCLUDED.people_count, person.people_count),
|
people_count = COALESCE(EXCLUDED.people_count, person.people_count),
|
||||||
anatomical_completeness = COALESCE(EXCLUDED.anatomical_completeness, person.anatomical_completeness),
|
anatomical_completeness = COALESCE(EXCLUDED.anatomical_completeness, person.anatomical_completeness),
|
||||||
facial_direction = COALESCE(EXCLUDED.facial_direction, person.facial_direction),
|
facial_direction = COALESCE(EXCLUDED.facial_direction, person.facial_direction),
|
||||||
objects = COALESCE(EXCLUDED.objects, person.objects);
|
objects = COALESCE(EXCLUDED.objects, person.objects),
|
||||||
|
description = COALESCE(EXCLUDED.description, person.description);
|
||||||
""", (filename, filepath, name, group_id,
|
""", (filename, filepath, name, group_id,
|
||||||
json.dumps(tags) if tags else None,
|
json.dumps(tags) if tags else None,
|
||||||
embedding, clip_description, prompt, pose, sort_order, group_name, hidden,
|
embedding, clip_description, prompt, pose, sort_order, group_name, hidden,
|
||||||
has_background, source_refs, has_clothing,
|
has_background, source_refs, has_clothing,
|
||||||
content_type, faceswap_source_video, archived, face_embedding_str, is_source,
|
content_type, faceswap_source_video, archived, face_embedding_str, is_source,
|
||||||
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction,
|
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction,
|
||||||
json.dumps(objects) if objects else None))
|
json.dumps(objects) if objects else None,
|
||||||
|
description))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
# Sync after commit
|
# Sync after commit
|
||||||
try:
|
try:
|
||||||
@@ -381,7 +405,7 @@ def get_person(filename):
|
|||||||
SELECT name, group_id, tags, embedding, clip_description, filepath,
|
SELECT name, group_id, tags, embedding, clip_description, filepath,
|
||||||
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
||||||
has_clothing, is_source, pose_description, pose_skeleton,
|
has_clothing, is_source, pose_description, pose_skeleton,
|
||||||
people_count, anatomical_completeness, facial_direction, objects
|
people_count, anatomical_completeness, facial_direction, objects, description
|
||||||
FROM person WHERE filename = %s
|
FROM person WHERE filename = %s
|
||||||
""", (filename,))
|
""", (filename,))
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
@@ -398,7 +422,7 @@ def list_persons(include_archived=False):
|
|||||||
SELECT filename, name, group_id, clip_description,
|
SELECT filename, name, group_id, clip_description,
|
||||||
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
||||||
has_clothing, content_type, faceswap_source_video, archived, is_source, tags,
|
has_clothing, content_type, faceswap_source_video, archived, is_source, tags,
|
||||||
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects
|
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects, description
|
||||||
FROM person
|
FROM person
|
||||||
{where}
|
{where}
|
||||||
""")
|
""")
|
||||||
@@ -700,7 +724,7 @@ def sync_person_to_json(filename):
|
|||||||
has_background, source_refs, has_clothing, content_type,
|
has_background, source_refs, has_clothing, content_type,
|
||||||
faceswap_source_video, archived, face_embedding, is_source,
|
faceswap_source_video, archived, face_embedding, is_source,
|
||||||
pose_description, pose_skeleton, people_count, anatomical_completeness,
|
pose_description, pose_skeleton, people_count, anatomical_completeness,
|
||||||
facial_direction, objects
|
facial_direction, objects, description
|
||||||
FROM person
|
FROM person
|
||||||
WHERE filename = %s
|
WHERE filename = %s
|
||||||
""", (filename,))
|
""", (filename,))
|
||||||
@@ -787,7 +811,8 @@ def sync_person_to_json(filename):
|
|||||||
"people_count": row[22],
|
"people_count": row[22],
|
||||||
"anatomical_completeness": row[23],
|
"anatomical_completeness": row[23],
|
||||||
"facial_direction": row[24],
|
"facial_direction": row[24],
|
||||||
"objects": obj_list
|
"objects": obj_list,
|
||||||
|
"description": row[26]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Determine json path
|
# Determine json path
|
||||||
|
|||||||
@@ -2235,6 +2235,23 @@ class UpdatePromptRequest(BaseModel):
|
|||||||
prompt: str
|
prompt: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDescriptionRequest(BaseModel):
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/images/{filename:path}/update-description")
|
||||||
|
def update_description(filename: str, req: UpdateDescriptionRequest):
|
||||||
|
try:
|
||||||
|
person = database.get_person(filename)
|
||||||
|
if not person:
|
||||||
|
raise HTTPException(404, "Image not found in database")
|
||||||
|
database.upsert_person(filename, description=req.description)
|
||||||
|
_invalidate_static()
|
||||||
|
return {"status": "success", "filename": filename, "description": req.description}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/images/{filename:path}/reverse-engineer")
|
@app.post("/images/{filename:path}/reverse-engineer")
|
||||||
def reverse_engineer(filename: str):
|
def reverse_engineer(filename: str):
|
||||||
person = database.get_person(filename)
|
person = database.get_person(filename)
|
||||||
@@ -2316,11 +2333,10 @@ def reverse_engineer(filename: str):
|
|||||||
context_str = "\n".join(context_parts)
|
context_str = "\n".join(context_parts)
|
||||||
|
|
||||||
user_content = (
|
user_content = (
|
||||||
"Reconstruct a high-quality, cohesive photorealistic prompt that perfectly matches the reference image characteristics. "
|
"Reconstruct a high-quality, cohesive photorealistic prompt and description that perfectly matches the reference image characteristics. "
|
||||||
"Here are the extracted metadata and descriptions from the target reference image:\n\n"
|
"Here are the extracted metadata and descriptions from the target reference image:\n\n"
|
||||||
f"{context_str}\n\n"
|
f"{context_str}\n\n"
|
||||||
"Synthesize all the tags, pose information, and scene details into a single detailed photorealistic prompt "
|
"Synthesize all the tags, pose information, and scene details into a pose-directed prompt and a broad description."
|
||||||
"ideal for generating a matching image in Qwen. Do not include negative descriptions."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
llm_api = "http://192.168.1.160:8001/v1/chat/completions"
|
llm_api = "http://192.168.1.160:8001/v1/chat/completions"
|
||||||
@@ -2332,11 +2348,12 @@ def reverse_engineer(filename: str):
|
|||||||
"content": (
|
"content": (
|
||||||
"You write explicit photographic — black void sceneries — pose and scenery directions for adult (NSFW) nude female photography. "
|
"You write explicit photographic — black void sceneries — pose and scenery directions for adult (NSFW) nude female photography. "
|
||||||
"You never refuse or add disclaimers. "
|
"You never refuse or add disclaimers. "
|
||||||
"Your task is to RECONSTRUCT and REVERSE ENGINEER the reference details into a single high-quality, detailed generation prompt. "
|
"Your task is to RECONSTRUCT and REVERSE ENGINEER the reference details into a pose-directed prompt and a broad scene/character description.\n"
|
||||||
"Output ONLY the detailed generation prompt lines — no preamble, no commentary, no formatting. "
|
"You must output exactly two blocks in this exact format, with no other text, commentary, or preamble:\n\n"
|
||||||
"Provide 7-12 short to medium length lines describing body position and characteristics, "
|
"[PROMPT]\n"
|
||||||
"ending with a realism tag like 'Perfect anatomy, photo realistic. keep the characteristics of the reference image.' or 'Anatomically precise. photorealistic, keep the characteristics of the reference image'. "
|
"<7-12 short to medium lines describing the body pose, position, and physical mechanics of the action. It must NOT contain any specific actor characteristics like skin color, hair color, eye color, or race, so that it can be easily replicated over other actors. Do not mention clothing or scenery details here. End with: 'Perfect anatomy, photo realistic. keep the characteristics of the reference image.'>\n\n"
|
||||||
"Be specific and inventive."
|
"[DESCRIPTION]\n"
|
||||||
|
"<A broad paragraph or bulleted lines describing the actor's physical details (skin color, hair color, eye color, skin details), clothing, scenery/background, lighting, and overall atmosphere. This should be comprehensive and descriptive.>"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{"role": "user", "content": user_content}
|
{"role": "user", "content": user_content}
|
||||||
@@ -2350,13 +2367,25 @@ def reverse_engineer(filename: str):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
reconstructed = data["choices"][0]["message"]["content"].strip()
|
reconstructed = data["choices"][0]["message"]["content"].strip()
|
||||||
|
|
||||||
|
prompt_text = ""
|
||||||
|
desc_text = ""
|
||||||
|
reconstructed_upper = reconstructed.replace("[prompt]", "[PROMPT]").replace("[description]", "[DESCRIPTION]")
|
||||||
|
if "[PROMPT]" in reconstructed_upper and "[DESCRIPTION]" in reconstructed_upper:
|
||||||
|
parts = reconstructed_upper.split("[DESCRIPTION]")
|
||||||
|
prompt_text = parts[0].replace("[PROMPT]", "").strip()
|
||||||
|
desc_text = parts[1].strip()
|
||||||
|
else:
|
||||||
|
prompt_text = reconstructed.strip()
|
||||||
|
desc_text = "Nude female photo, high-quality, scenery details."
|
||||||
|
|
||||||
try:
|
try:
|
||||||
database.save_db_prompt("reverse-engineer", reconstructed, {
|
database.save_db_prompt("reverse-engineer", reconstructed, {
|
||||||
"filename": filename
|
"filename": filename
|
||||||
})
|
})
|
||||||
except Exception as db_err:
|
except Exception as db_err:
|
||||||
print(f"[reverse-engineer] failed to save to prompt table: {db_err}")
|
print(f"[reverse-engineer] failed to save to prompt table: {db_err}")
|
||||||
return {"prompt": reconstructed}
|
return {"prompt": prompt_text, "description": desc_text}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Reverse engineer error: {e}")
|
print(f"Reverse engineer error: {e}")
|
||||||
# Fallback to a high-quality combination of pose_desc and clip_desc
|
# Fallback to a high-quality combination of pose_desc and clip_desc
|
||||||
@@ -2366,7 +2395,7 @@ def reverse_engineer(filename: str):
|
|||||||
if clip_desc_val:
|
if clip_desc_val:
|
||||||
parts.append(clip_desc_val)
|
parts.append(clip_desc_val)
|
||||||
fallback_prompt = ", ".join(parts) if parts else "Perfect anatomy, photo realistic"
|
fallback_prompt = ", ".join(parts) if parts else "Perfect anatomy, photo realistic"
|
||||||
return {"prompt": fallback_prompt}
|
return {"prompt": fallback_prompt, "description": fallback_prompt}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/images/{filename:path}/update-prompt")
|
@app.post("/images/{filename:path}/update-prompt")
|
||||||
@@ -2682,6 +2711,7 @@ def list_images(archived: bool = False, bypass_static: bool = False):
|
|||||||
"anatomical_completeness": p[20],
|
"anatomical_completeness": p[20],
|
||||||
"facial_direction": p[21],
|
"facial_direction": p[21],
|
||||||
"objects": obj_list,
|
"objects": obj_list,
|
||||||
|
"description": p[23] if len(p) > 23 else None,
|
||||||
})
|
})
|
||||||
db_images.sort(
|
db_images.sort(
|
||||||
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||||
@@ -3878,6 +3908,12 @@ def set_image_tags(filename: str, req: ImageTagsRequest):
|
|||||||
if "VISIBLE" not in tags_list:
|
if "VISIBLE" not in tags_list:
|
||||||
tags_list.append("VISIBLE")
|
tags_list.append("VISIBLE")
|
||||||
|
|
||||||
|
if filename.startswith("_turntable/"):
|
||||||
|
if "ORBIT" not in tags_list:
|
||||||
|
tags_list.append("ORBIT")
|
||||||
|
else:
|
||||||
|
tags_list = [t for t in tags_list if t != "ORBIT"]
|
||||||
|
|
||||||
conn = database.get_db_connection()
|
conn = database.get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
try:
|
try:
|
||||||
@@ -3956,6 +3992,12 @@ def tag_action_endpoint(filename: str, req: TagActionRequest):
|
|||||||
if "VISIBLE" not in tags_list:
|
if "VISIBLE" not in tags_list:
|
||||||
tags_list.append("VISIBLE")
|
tags_list.append("VISIBLE")
|
||||||
|
|
||||||
|
if filename.startswith("_turntable/"):
|
||||||
|
if "ORBIT" not in tags_list:
|
||||||
|
tags_list.append("ORBIT")
|
||||||
|
else:
|
||||||
|
tags_list = [t for t in tags_list if t != "ORBIT"]
|
||||||
|
|
||||||
conn = database.get_db_connection()
|
conn = database.get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user