updates UI
This commit is contained in:
@@ -1855,9 +1855,47 @@
|
||||
|
||||
/* ===================== SEGMENT TAB ===================== */
|
||||
.segment-checker-hint { font-size: 10px; color: #444; margin-top: 6px; }
|
||||
|
||||
/* ===================== SERVICES STATUS LEDS ===================== */
|
||||
.status-dot-led {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.status-dot-led.online {
|
||||
background-color: #22c55e;
|
||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
|
||||
animation: status-pulse 2s infinite ease-in-out;
|
||||
}
|
||||
.status-dot-led.busy {
|
||||
background-color: #3b82f6;
|
||||
box-shadow: 0 0 8px rgba(59, 130, 246, 0.6);
|
||||
animation: status-pulse 0.8s infinite ease-in-out;
|
||||
}
|
||||
.status-dot-led.offline {
|
||||
background-color: #ef4444;
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
||||
}
|
||||
@keyframes status-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.9); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 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 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>API</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 6px; cursor: help;" id="modelStatus" title="Model-Engine: Status unknown (API server is down)">
|
||||
<span class="status-dot-led offline" id="modelDot"></span>
|
||||
<span>Model</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="privacyOverlay" class="privacy-overlay" onclick="if(privacyMode)disablePrivacyOverlay()">
|
||||
<div class="priv-topbar">
|
||||
<div class="priv-logo">S</div>
|
||||
@@ -2383,6 +2421,17 @@
|
||||
|
||||
// --- HYDRATION_START ---
|
||||
const PRELOADED_IMAGES = [
|
||||
"20260701_031014_4_20260618_052526_image.png",
|
||||
"20260701_030956_4_20260618_052526_image.png",
|
||||
"20260701_030939_4_20260618_052526_image.png",
|
||||
"20260701_030922_4_20260618_052526_image.png",
|
||||
"20260701_030906_4_20260618_052526_image.png",
|
||||
"20260701_025821_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"20260701_025804_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"20260701_025747_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"20260701_025731_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"20260701_025714_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"20260701_025651_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"image.png_face.png",
|
||||
"20260628_235923_3_20260628_205049_image.nobg.png",
|
||||
"20260701_005145_sc_image.png",
|
||||
@@ -8053,7 +8102,8 @@
|
||||
|
||||
function isOrbitFile(fname) {
|
||||
if (!fname) return false;
|
||||
return fname.includes('_turntable/') || /deg\.png|deg_|\d+deg/i.test(fname);
|
||||
const tags = fileTags[fname] || [];
|
||||
return fname.includes('_turntable/') || tags.includes('ORBIT');
|
||||
}
|
||||
|
||||
function computeVisibleIdx(names) {
|
||||
@@ -9790,6 +9840,10 @@
|
||||
}
|
||||
|
||||
async function lbAutoCrop() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot crop: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
const btn = document.getElementById('lbCropBtn');
|
||||
@@ -9808,6 +9862,10 @@
|
||||
}
|
||||
|
||||
async function lbRotate(degrees) {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot rotate: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to rotate', 'info'); return; }
|
||||
const btns = [document.getElementById('lbRotateLeftBtn'), document.getElementById('lbRotateRightBtn')];
|
||||
@@ -9842,6 +9900,14 @@
|
||||
}
|
||||
|
||||
async function lbTogglePose() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot estimate pose: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
if (!_modelEngineOnline) {
|
||||
showToast("Cannot estimate pose: Model-Engine is offline", "error");
|
||||
return;
|
||||
}
|
||||
if (document.getElementById('poseCanvas')) { _removePoseOverlay(); return; }
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image', 'info'); return; }
|
||||
@@ -10459,6 +10525,10 @@
|
||||
}
|
||||
|
||||
function startManualCrop() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot crop: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to crop', 'info'); return; }
|
||||
const viewer = document.getElementById('studioViewer');
|
||||
@@ -10615,6 +10685,10 @@
|
||||
}
|
||||
|
||||
function startManualPad() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot pad: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to pad', 'info'); return; }
|
||||
const viewer = document.getElementById('studioViewer');
|
||||
@@ -10740,6 +10814,10 @@
|
||||
}
|
||||
|
||||
async function lbDuplicate() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot duplicate: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
const btn = document.getElementById('lbDuplicateBtn');
|
||||
@@ -10797,6 +10875,10 @@
|
||||
}
|
||||
|
||||
async function lbRemoveBg() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot remove background: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
const btn = document.getElementById('lbNoBgBtn') || document.getElementById('sbNoBgBtn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Removing…'; }
|
||||
@@ -10819,6 +10901,10 @@
|
||||
}
|
||||
|
||||
async function lbInvertAlpha() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot invert alpha: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname || !/\.(png|webp)$/i.test(fname)) { showToast('Invert needs a transparent PNG', 'info'); return; }
|
||||
const btn = document.getElementById('lbInvertAlphaBtn');
|
||||
@@ -10838,6 +10924,10 @@
|
||||
}
|
||||
|
||||
async function lbMakeVideo() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot generate video: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
// Use Shift+Click selected refs if any, otherwise all images in the current group.
|
||||
let filenames;
|
||||
if (_sbRefIndices.length >= 2) {
|
||||
@@ -11640,7 +11730,11 @@
|
||||
// Primary: pre-generated static JSON (fast, no DB round-trip per request)
|
||||
try {
|
||||
if (bypassStatic) throw new Error("Bypass static requested");
|
||||
const r = await fetch(`${API}/output/_data/images.json?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) });
|
||||
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 r = await fetch(jsonUrl, { signal: AbortSignal.timeout(4000) });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
let imgs = data.images || [];
|
||||
@@ -11654,7 +11748,10 @@
|
||||
// Fallback: live API (handles first startup before static file is written)
|
||||
if (!files) {
|
||||
try {
|
||||
const url = `${API}/images?archived=true${bypassStatic ? '&bypass_static=true' : ''}`;
|
||||
let url = `${API}/images?archived=true${bypassStatic ? '&bypass_static=true' : ''}`;
|
||||
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||
url += `&group_id=${encodeURIComponent(LOAD_ONLY_GROUP_ID)}`;
|
||||
}
|
||||
const r = await fetch(url, { signal: AbortSignal.timeout(12000) });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
@@ -12055,6 +12152,26 @@
|
||||
if (statusDot) statusDot.classList.remove('updating');
|
||||
updateGalleryHighlight();
|
||||
loadImages._inFlight = false;
|
||||
|
||||
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
|
||||
const studio = document.getElementById('studio');
|
||||
if (studio && !studio.classList.contains('open')) {
|
||||
const header = document.querySelector('.header');
|
||||
if (header) header.style.display = 'none';
|
||||
const galleryEl = document.getElementById('gallery');
|
||||
if (galleryEl) galleryEl.style.display = 'none';
|
||||
|
||||
const galleryBtn = document.querySelector('.studio-topbar-left button');
|
||||
if (galleryBtn && galleryBtn.textContent.includes('Gallery')) {
|
||||
galleryBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
if (groupData.has(LOAD_ONLY_GROUP_ID)) {
|
||||
openStudio(LOAD_ONLY_GROUP_ID, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (loadImages._needsRerun) {
|
||||
loadImages._needsRerun = false;
|
||||
const bp = !!loadImages._pendingBypass;
|
||||
@@ -12158,6 +12275,55 @@
|
||||
}
|
||||
|
||||
const API = 'http://127.0.0.1:8500';
|
||||
const LOAD_ONLY_GROUP_ID = null;
|
||||
|
||||
let _backendOnline = true;
|
||||
let _modelEngineOnline = true;
|
||||
|
||||
async function pollServicesStatus() {
|
||||
try {
|
||||
const r = await fetch(`${API}/services/status`, { signal: AbortSignal.timeout(2000) });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
_backendOnline = true;
|
||||
_modelEngineOnline = data.model_engine.online;
|
||||
|
||||
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)';
|
||||
}
|
||||
|
||||
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)';
|
||||
}
|
||||
} else {
|
||||
throw new Error("API responded with error");
|
||||
}
|
||||
} catch (e) {
|
||||
_backendOnline = false;
|
||||
_modelEngineOnline = false;
|
||||
|
||||
const backendDot = document.getElementById('backendDot');
|
||||
if (backendDot) {
|
||||
backendDot.className = 'status-dot-led offline';
|
||||
backendDot.parentElement.title = 'Backend: Offline (API server is down)';
|
||||
}
|
||||
const modelDot = document.getElementById('modelDot');
|
||||
if (modelDot) {
|
||||
modelDot.className = 'status-dot-led offline';
|
||||
modelDot.parentElement.title = 'Model-Engine: Status unknown (API server is down)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling services status every 3 seconds
|
||||
setInterval(pollServicesStatus, 3000);
|
||||
setTimeout(pollServicesStatus, 100);
|
||||
|
||||
// --- selection state ---
|
||||
let selectionMode = false;
|
||||
@@ -13776,6 +13942,14 @@
|
||||
}
|
||||
|
||||
async function submitSbGenerate() {
|
||||
if (!_backendOnline) {
|
||||
showToast("Cannot generate: Backend API is offline", "error");
|
||||
return;
|
||||
}
|
||||
if (!_modelEngineOnline) {
|
||||
showToast("Cannot generate: Model-Engine (ComfyUI) is offline", "error");
|
||||
return;
|
||||
}
|
||||
if (!_fsModelFilename) return;
|
||||
const promptVal = (document.getElementById('sbGenPromptInput')?.value || '').trim();
|
||||
const hasPoses = _fsSelectedPoses.size > 0 || _sbSelectedAngles.size > 0;
|
||||
|
||||
@@ -112,6 +112,23 @@ def migrate_schema():
|
||||
CONSTRAINT unique_prompt_type_text UNIQUE (type, prompt_text)
|
||||
)
|
||||
""")
|
||||
# Create turntable table
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS turntable (
|
||||
group_id TEXT PRIMARY KEY,
|
||||
preferred_filename TEXT,
|
||||
source_image TEXT,
|
||||
seed INTEGER,
|
||||
n_views INTEGER,
|
||||
steps INTEGER,
|
||||
video_path TEXT,
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
started_at DOUBLE PRECISION,
|
||||
completed_at DOUBLE PRECISION,
|
||||
angles TEXT, -- JSON list of angles
|
||||
views TEXT -- JSON representation of views dict
|
||||
)
|
||||
""")
|
||||
for sql in [
|
||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS prompt TEXT",
|
||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS pose TEXT",
|
||||
@@ -142,6 +159,10 @@ def migrate_schema():
|
||||
initialize_tags_in_db()
|
||||
except Exception as e:
|
||||
print(f"[db] initialize_tags_in_db error: {e}")
|
||||
try:
|
||||
migrate_turntable_states_from_disk_to_db()
|
||||
except Exception as e:
|
||||
print(f"[db] migrate_turntable_states_from_disk_to_db error: {e}")
|
||||
|
||||
def initialize_tags_in_db():
|
||||
conn = get_db_connection()
|
||||
@@ -214,6 +235,14 @@ def initialize_tags_in_db():
|
||||
if "BACKGROUND_REMOVED" in tags_list:
|
||||
tags_list.remove("BACKGROUND_REMOVED")
|
||||
|
||||
# ORBIT tag logic: only true orbits are tagged with ORBIT
|
||||
is_orb = filename.startswith("_turntable/")
|
||||
if is_orb:
|
||||
add_tag_if_missing("ORBIT")
|
||||
else:
|
||||
if "ORBIT" in tags_list:
|
||||
tags_list.remove("ORBIT")
|
||||
|
||||
cur.execute("UPDATE person SET tags = %s WHERE filename = %s", (json.dumps(tags_list), filename))
|
||||
conn.commit()
|
||||
finally:
|
||||
@@ -1006,3 +1035,134 @@ def list_db_prompts(prompt_type: str = None, limit: int = 100):
|
||||
finally:
|
||||
cur.close()
|
||||
_put_db_connection(conn)
|
||||
|
||||
|
||||
def save_turntable(group_id, state):
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO turntable (group_id, preferred_filename, source_image, seed, n_views, steps, video_path, completed, started_at, completed_at, angles, views)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (group_id) DO UPDATE SET
|
||||
preferred_filename = EXCLUDED.preferred_filename,
|
||||
source_image = EXCLUDED.source_image,
|
||||
seed = EXCLUDED.seed,
|
||||
n_views = EXCLUDED.n_views,
|
||||
steps = EXCLUDED.steps,
|
||||
video_path = EXCLUDED.video_path,
|
||||
completed = EXCLUDED.completed,
|
||||
started_at = EXCLUDED.started_at,
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
angles = EXCLUDED.angles,
|
||||
views = EXCLUDED.views
|
||||
""", (
|
||||
group_id,
|
||||
state.get("preferred_filename"),
|
||||
state.get("source_image"),
|
||||
state.get("seed"),
|
||||
state.get("n_views"),
|
||||
state.get("steps"),
|
||||
state.get("video_path"),
|
||||
state.get("completed", False),
|
||||
state.get("started_at"),
|
||||
state.get("completed_at"),
|
||||
json.dumps(state.get("angles", [])),
|
||||
json.dumps(state.get("views", {}))
|
||||
))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f"[db] error saving turntable for {group_id}: {e}")
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
cur.close()
|
||||
_put_db_connection(conn)
|
||||
|
||||
|
||||
def load_turntable(group_id):
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT group_id, preferred_filename, source_image, seed, n_views, steps, video_path, completed, started_at, completed_at, angles, views
|
||||
FROM turntable
|
||||
WHERE group_id = %s
|
||||
""", (group_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
angles_val = []
|
||||
if row[10]:
|
||||
try:
|
||||
angles_val = json.loads(row[10])
|
||||
except Exception:
|
||||
angles_val = []
|
||||
|
||||
views_val = {}
|
||||
if row[11]:
|
||||
try:
|
||||
views_val = json.loads(row[11])
|
||||
except Exception:
|
||||
views_val = {}
|
||||
|
||||
return {
|
||||
"group_id": row[0],
|
||||
"preferred_filename": row[1],
|
||||
"source_image": row[2],
|
||||
"seed": row[3],
|
||||
"n_views": row[4],
|
||||
"steps": row[5],
|
||||
"video_path": row[6],
|
||||
"completed": bool(row[7]) if row[7] is not None else False,
|
||||
"started_at": row[8],
|
||||
"completed_at": row[9],
|
||||
"angles": angles_val,
|
||||
"views": views_val
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[db] error loading turntable for {group_id}: {e}")
|
||||
return None
|
||||
finally:
|
||||
cur.close()
|
||||
_put_db_connection(conn)
|
||||
|
||||
|
||||
def delete_turntable(group_id):
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("DELETE FROM turntable WHERE group_id = %s", (group_id,))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f"[db] error deleting turntable for {group_id}: {e}")
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
cur.close()
|
||||
_put_db_connection(conn)
|
||||
|
||||
|
||||
def migrate_turntable_states_from_disk_to_db():
|
||||
try:
|
||||
out_dir = _get_output_dir()
|
||||
td = os.path.join(out_dir, "_turntable")
|
||||
if not os.path.isdir(td):
|
||||
return
|
||||
gids = [d for d in os.listdir(td) if os.path.isfile(os.path.join(td, d, "state.json"))]
|
||||
for gid in gids:
|
||||
p = os.path.join(td, gid, "state.json")
|
||||
try:
|
||||
with open(p, "r") as f:
|
||||
state = json.load(f)
|
||||
if state:
|
||||
save_turntable(gid, state)
|
||||
except Exception as e:
|
||||
print(f"[db] Error migrating turntable state {gid} from disk: {e}")
|
||||
except Exception as e:
|
||||
print(f"[db] Error in migrate_turntable_states_from_disk_to_db: {e}")
|
||||
|
||||
@@ -442,7 +442,8 @@ def _idle_turntable_daemon():
|
||||
prompt=prompt,
|
||||
source_refs=json.dumps([preferred_fname]),
|
||||
sort_order=200 + angle_idx,
|
||||
pose=f"Orbit {int(deg)}°"
|
||||
pose=f"Orbit {int(deg)}°",
|
||||
tags=["ORBIT"]
|
||||
)
|
||||
_update_cached_file_meta(vname, exists=True)
|
||||
except Exception as db_err:
|
||||
@@ -1551,6 +1552,47 @@ def _write_all_static() -> None:
|
||||
# Update car.html PRELOADED_IMAGES
|
||||
_sync_preloaded_images()
|
||||
|
||||
# Group-specific JSON and HTML generation
|
||||
car_html_src = os.path.join(_HERE, "car.html")
|
||||
template_content = ""
|
||||
if os.path.exists(car_html_src):
|
||||
try:
|
||||
with open(car_html_src, "r", encoding="utf-8") as f:
|
||||
template_content = f.read()
|
||||
except Exception as template_err:
|
||||
print(f"[static] Error reading car.html template: {template_err}")
|
||||
|
||||
# Extract unique group IDs from db_images
|
||||
gids = set(p["group_id"] for p in db_images if p.get("group_id"))
|
||||
for gid in gids:
|
||||
if not gid:
|
||||
continue
|
||||
# Sanitize the group_id to prevent directory traversal / invalid characters in filenames
|
||||
sanitized_gid = re.sub(r'[^a-zA-Z0-9_\-]', '_', gid)
|
||||
|
||||
# Filter images belonging to this group
|
||||
group_images = [p for p in db_images if p.get("group_id") == gid]
|
||||
|
||||
# Write group json data: _data/group_{sanitized_gid}.json
|
||||
group_json_path = os.path.join(data_dir, f"group_{sanitized_gid}.json")
|
||||
_write_json(group_json_path, {"images": group_images})
|
||||
|
||||
# Write shoot specific html: shoot_{sanitized_gid}.html next to car.html
|
||||
if template_content:
|
||||
# Replace `const LOAD_ONLY_GROUP_ID = null;` with `const LOAD_ONLY_GROUP_ID = '{gid}';`
|
||||
escaped_gid = gid.replace("'", "\\'").replace('"', '\\"')
|
||||
replaced_content = template_content.replace(
|
||||
"const LOAD_ONLY_GROUP_ID = null;",
|
||||
f"const LOAD_ONLY_GROUP_ID = '{escaped_gid}';"
|
||||
)
|
||||
|
||||
group_html_path = os.path.join(output_dir, f"shoot_{sanitized_gid}.html")
|
||||
try:
|
||||
with open(group_html_path, "w", encoding="utf-8") as f:
|
||||
f.write(replaced_content)
|
||||
except Exception as html_err:
|
||||
print(f"[static] Error writing group html for {gid}: {html_err}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[static] write_all error: {e}")
|
||||
|
||||
@@ -3396,6 +3438,54 @@ def health():
|
||||
raise HTTPException(503, f"ComfyUI unreachable at {COMFY}: {e}")
|
||||
|
||||
|
||||
@app.get("/services/status")
|
||||
def get_services_status():
|
||||
comfy_online = False
|
||||
comfy_busy = False
|
||||
|
||||
# 1. Check ComfyUI Model-Engine
|
||||
try:
|
||||
r = requests.get(f"{COMFY}/system_stats", timeout=1.0)
|
||||
if r.status_code == 200:
|
||||
comfy_online = True
|
||||
# Check if there are active queue items in ComfyUI
|
||||
try:
|
||||
qr = requests.get(f"{COMFY}/queue", timeout=1.0)
|
||||
if qr.status_code == 200:
|
||||
qj = qr.json()
|
||||
running = qj.get("queue_running", [])
|
||||
pending = qj.get("queue_pending", [])
|
||||
if running or pending:
|
||||
comfy_busy = True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Check if backend is busy with its own tasks
|
||||
backend_busy = False
|
||||
for jid, job in jobs.items():
|
||||
if job.get("status") == "running":
|
||||
backend_busy = True
|
||||
break
|
||||
|
||||
# Or is turntable active?
|
||||
if _idle_turntable_busy:
|
||||
backend_busy = True
|
||||
comfy_busy = True # Since turntable runs on ComfyUI
|
||||
|
||||
return {
|
||||
"backend": {
|
||||
"online": True,
|
||||
"busy": backend_busy
|
||||
},
|
||||
"model_engine": {
|
||||
"online": comfy_online,
|
||||
"busy": comfy_busy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/privacy/status")
|
||||
def get_privacy_status():
|
||||
return {"locked": _privacy_locked}
|
||||
@@ -6363,7 +6453,8 @@ def _create_qwen_orbit(req: OrbitRequest, output_dir: str, img_path: str) -> dic
|
||||
prompt=yaw_prompt(v["deg"]),
|
||||
source_refs=json.dumps([req.filename]),
|
||||
sort_order=200 + angle_idx,
|
||||
pose=f"Orbit {int(v['deg'])}°"
|
||||
pose=f"Orbit {int(v['deg'])}°",
|
||||
tags=["ORBIT"]
|
||||
)
|
||||
except Exception as db_err:
|
||||
print(f"[orbit-qwen] DB frame error: {db_err}")
|
||||
|
||||
@@ -26,6 +26,14 @@ def state_path(output_dir: str, group_id: str) -> str:
|
||||
|
||||
|
||||
def load_state(output_dir: str, group_id: str) -> Optional[dict]:
|
||||
try:
|
||||
import database
|
||||
db_state = database.load_turntable(group_id)
|
||||
if db_state:
|
||||
return db_state
|
||||
except Exception as e:
|
||||
print(f"[turntable_cache] DB load error: {e}")
|
||||
|
||||
p = state_path(output_dir, group_id)
|
||||
if not os.path.exists(p):
|
||||
return None
|
||||
@@ -44,6 +52,12 @@ def save_state(output_dir: str, group_id: str, state: dict):
|
||||
json.dump(state, f, indent=2)
|
||||
os.replace(tmp, p) # atomic
|
||||
|
||||
try:
|
||||
import database
|
||||
database.save_turntable(group_id, state)
|
||||
except Exception as e:
|
||||
print(f"[turntable_cache] DB save error: {e}")
|
||||
|
||||
|
||||
def init_state(
|
||||
output_dir: str,
|
||||
@@ -139,6 +153,12 @@ def delete_state(output_dir: str, group_id: str):
|
||||
if os.path.isdir(d):
|
||||
shutil.rmtree(d)
|
||||
|
||||
try:
|
||||
import database
|
||||
database.delete_turntable(group_id)
|
||||
except Exception as e:
|
||||
print(f"[turntable_cache] DB delete error: {e}")
|
||||
|
||||
|
||||
def get_group_video(output_dir: str, group_id: str) -> Optional[str]:
|
||||
"""Return the video path if the turntable is complete and the file exists."""
|
||||
|
||||
Reference in New Issue
Block a user