+
+
+
-
-
-
-
-
-
-
-
-
Download
-
-
+
+
+
AI & Export
+
+
+
+
+
+
+
+
+
+
+
File Management
+
Image name
-
+
+
+
+
All
+
@@ -2421,6 +2503,11 @@
// --- HYDRATION_START ---
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_030956_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_232800_2_20260618_052526_image.nobg.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_163400_image.png",
"20260630_163247_3_20260628_205049_image.png",
@@ -2523,13 +2609,9 @@
"20260630_151554_sc_0_20260622_101128_image.png",
"20260630_151502_sc_0_20260622_101128_image.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_131638_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_130601_sc_4_20260622_101128_image.png",
- "20260630_130454_sc_0_20260622_101128_image.png",
"20260630_130350_sc_0_20260622_101128_image.png",
"20260630_130220_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_060725_1_20260629_060652_image.png",
"20260629_060714_0_20260629_060652_image.png",
- "20260629_060652_image.png",
"cg_fb1043d8_face.png",
"20260629_055242_image.png",
"_turntable/cg_2bbb563a/views/view_022_330deg.png",
@@ -5832,10 +5913,6 @@
"20260626_005504_Pasted image (9).png",
"20260626_005322_dup_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_223304_p12.png",
"20260625_210437_image.png",
@@ -7876,12 +7953,14 @@
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
let _prevGid = null; // for "Back" button in studio
let imageNames = {}; // filename β human name (from /names)
+ let fileFilepaths = {}; // filename β absolute path on disk
let clipDescriptions = {}; // filename β description
let customGroups = {}; // filename β groupId (from /groups)
let groupNames = {}; // group_id β group display name
let fileSortOrders = {}; // filename β sort_order (integer or null)
let filePoses = {}; // filename β pose name
let filePrompts = {}; // filename β generation prompt
+ let fileDescriptions = {}; // filename β custom description
let fileHidden = {}; // filename β boolean (hidden from cycling)
let fileArchived = {}; // filename β boolean
let filePoseDescription = {}; // filename β human readable string
@@ -8911,6 +8990,10 @@
}
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.body.style.overflow = '';
stopSlideshow();
@@ -9176,11 +9259,34 @@
const clipEl = document.getElementById('lbClipDesc');
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 genPromptEl = document.getElementById('lbGenPrompt');
- const genPrompt = filePrompts[fname] || '';
- if (genPromptWrap) genPromptWrap.style.display = genPrompt ? '' : 'none';
- if (genPromptEl) genPromptEl.textContent = genPrompt;
+ if (genPromptWrap && genPromptEl) {
+ genPromptWrap.style.display = fname ? '' : 'none';
+ 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 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() {
if (!_backendOnline) {
showToast("Cannot remove background: Backend API is offline", "error");
@@ -11350,6 +11471,13 @@
if (!r.ok) { panel.innerHTML = '
No turntables yet.
'; return; }
const { turntables } = await r.json();
_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(_) {
panel.innerHTML = '
No turntables yet.
';
return;
@@ -11689,6 +11817,8 @@
if (img.clip_description)clipDescriptions[img.filename]= img.clip_description;
if (img.pose) filePoses[img.filename] = img.pose;
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;
fileSortOrders[img.filename] = img.sort_order ?? null;
fileHidden[img.filename] = !!img.hidden;
@@ -11732,7 +11862,8 @@
if (bypassStatic) throw new Error("Bypass static requested");
let jsonUrl = `${API}/output/_data/images.json?t=${Date.now()}`;
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) });
if (r.ok) {
@@ -12319,6 +12450,19 @@
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
@@ -12467,7 +12611,12 @@
toggleFile(gid);
}
} 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';
const showMR = (n >= 2 && n <= 3);
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');
if (legend) {
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() {
currentGroups.forEach(g => selectedFiles.add(g.gid));
updateCardSelection();
@@ -12827,6 +12998,12 @@
const pBtn = document.getElementById('privacyBtn');
if (privacyMode && pBtn) {
pBtn.textContent = 'π';
+ }
+ const sBtn = document.getElementById('studioPrivacyBtn');
+ if (privacyMode && sBtn) {
+ sBtn.textContent = 'π';
+ }
+ if (privacyMode) {
document.title = PRIV_TITLE;
showPrivacyOverlay();
}
@@ -12892,7 +13069,7 @@
async function suggestUpdatedDescription() {
const fname = lbNames[lbIdx];
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 {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/reverse-engineer`, {
method: 'POST'
@@ -12900,23 +13077,31 @@
if (r.ok) {
const data = await r.json();
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 updateResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-prompt`, {
+ const newDesc = data.description;
+
+ 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',
headers: { 'Content-Type': 'application/json' },
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;
- showToast('Successfully updated image prompt!', 'success');
+ fileDescriptions[fname] = newDesc;
+ showToast('Successfully updated both prompt and description!', 'success');
updateStudio();
refreshNow();
} else {
- showToast('Failed to update image prompt', 'error');
+ showToast('Failed to update prompt or description', 'error');
}
}
} else {
- showToast('Failed to generate suggest description: ' + await r.text(), 'error');
+ showToast('Failed to generate suggestions: ' + await r.text(), 'error');
}
} catch (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 = `
+
+
+
+
+
+
+ `;
+
+ 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() {
const st = window._poseState;
const container = document.getElementById('poseRatioSuggestion');
@@ -13167,6 +13459,26 @@
window.addEventListener('blur', () => { 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', () => {
if (document.hidden) {
if (privacyMode) showPrivacyOverlay();
@@ -13180,9 +13492,12 @@
privacyMode = !privacyMode;
localStorage.setItem('privacyMode', privacyMode);
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) {
document.title = PRIV_TITLE; // keep the taskbar label discreet even while active
+ showPrivacyOverlay();
} else {
document.title = REAL_TITLE;
document.getElementById('privacyOverlay').style.display = 'none';
@@ -13752,6 +14067,7 @@
const nSafe = a.name.replace(/'/g, "\\'");
return `
`;
}).join('') + `
+