diff --git a/backlog.md b/backlog.md index 680b1c5..185db24 100644 --- a/backlog.md +++ b/backlog.md @@ -98,7 +98,7 @@ Scenery: - in /mnt/zim/tour-comfy/output/_turntable we have data regarding orbit generations. We want to store the orbit data in the database, start by adding the corresponding tags and meta info to the images (currently stored in the person table (but actually are images with metadata)) - in the ui - studio view we have an orbit filter. Currently the filter is filtering too many files, also references of orbit files are being marked orbit. When an orbit file is used, the metadata should not include the orbit tag. ## refine - +#file:///mnt/zim/tour-comfy/output/shoot_cg_7ec17537.html - when refresh page, we lose track of current jobs running. - generating poses themself should use the adviced dimensions rather than the base image reference. - introduce a like/dislike base system for images/group to determine the sort order (and more), would also dislike the pose @@ -151,4 +151,22 @@ In the app we looking at groups of images, so also keep track of the group, this 3) In the Design tab we will refine new poses. Often we want to reverse ingineer an existing image. Find a way/add a llm-model or reuse the VID or wd tagger or w/e to get something similar from an image we that we provide to qwen to generate images. We could also use this feature in the Info tab to suggest an updated description of the image. 4) Given a POSE, we want to estimate the image-size-ratio of the corresponding end-pose-skeleton to estimate if we need to pad the image before the actual image generation, try to see if we can fit this feature in somewhere, for now we will only do a suggestion based on that information. 5) make the faceswap preview mode fixed size instead of 50% we dont know how big the original clip is. just 470p or something standard. -6) on the landing page we now just play all images of an group. However if the orbit generaition is complete. I want to circulate (semi-video-view) those images (small in a corner or something of that group card) (and filtered out of the rest of the circulating images.) (same for the face feature to show that static on the group card.) \ No newline at end of file +6) on the landing page we now just play all images of an group. However if the orbit generaition is complete. I want to circulate (semi-video-view) those images (small in a corner or something of that group card) (and filtered out of the rest of the circulating images.) (same for the face feature to show that static on the group card.) + + +ok great. shoot-specifc html pages. have the orbit tab showing orbit of other shoots. +for now the orbit tab should only should orbits of the current group, which we can in the future add multiple per group. +So for now just filter the shoot specific pages orbits per group. +Then when we select multipel in the dashboard navigate tot he studio-view and show the selected orbit-groups in the orbit tab. +But when we sleect a single group in the dashboard navigate to the group specific html page and only show the orbit(s) of that group. + +the the status legs are great, they only hide the upload field in the landing page. and the hamburger/toolba in the specific group pages. + +What we mis in the shoot specific html page is hte privacy feature. we need to integrate that lock icon there. +If we lock the shoot specific html page, it should amke the entire app in privacy mode and set the other opened tabs in that state as well. + +then in the info tab we can perform the "Suggest Description" action. +introduce a new db field in the "person" table called "description". show both description and prompt in the info tab. and both allow refinement/ creation (if empty) +however the prompt should be directed towards the pose of the image and be replicated over other actors (so no skin color, hair color). The description should be more broad to also include the scenery and character details of the actor(s) like skin color, hair color. + +furhtermore, lets clean up the actions section in the "Info" tab \ No newline at end of file diff --git a/tour-comfy/car.html b/tour-comfy/car.html index fa64f87..d226b4e 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -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); } + } + +
+
+ + API +
+
+ + Model +
+
@@ -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; diff --git a/tour-comfy/database.py b/tour-comfy/database.py index fe998c2..0043dcb 100644 --- a/tour-comfy/database.py +++ b/tour-comfy/database.py @@ -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}") diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py index ab6e1c4..1428089 100644 --- a/tour-comfy/edit_api.py +++ b/tour-comfy/edit_api.py @@ -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}") diff --git a/tour-comfy/turntable_cache.py b/tour-comfy/turntable_cache.py index e332811..bcaaa90 100644 --- a/tour-comfy/turntable_cache.py +++ b/tour-comfy/turntable_cache.py @@ -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."""