From 139785d10864f57dc1568bd471a057abfb4cdd26 Mon Sep 17 00:00:00 2001 From: mike Date: Mon, 29 Jun 2026 12:56:31 +0200 Subject: [PATCH] updates UI --- backlog.md | 4 +- tour-comfy/car.html | 270 ++++++++++++++++++++++++------ tour-comfy/edit_api.py | 22 ++- tour-comfy/test_regression_api.py | 24 +++ 4 files changed, 262 insertions(+), 58 deletions(-) diff --git a/backlog.md b/backlog.md index accee85..72f984e 100644 --- a/backlog.md +++ b/backlog.md @@ -72,7 +72,9 @@ Scenery: - we can rate an image if its OVER21, manually now in the UI. this is fine, however we want to backfill this TAG for evey image - backfill, people-count, 21+, IS_COMPLETE, RESTRAINT - the wireframe / videos / sub-clips worden niet direct weergegeven -- +- make the faceswap preview mode fixed size instead of 50% we dont know how big the original clip is. just 470p or something. +- include 2 filter(s) in the filmstrip for VIDEO and ORBIT only show them if the sub-set is not empty. +- add a 2nd visible filmstrip to easy move items from one to another strip in the UI ## refine - when refresh page, we lose track of current jobs running. diff --git a/tour-comfy/car.html b/tour-comfy/car.html index 2f793e4..a176477 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -2334,8 +2334,13 @@ // --- HYDRATION_START --- const PRELOADED_IMAGES = [ + "20260629_124948_sc_view_023_345deg.png", + "20260629_124523_sc_view_023_345deg.png", + "20260629_124433_sc_view_023_345deg.png", + "20260629_122907_fs_Step Sis will do anything to make me Delete this Videos_1633s-2269s_image.png_prev50.mp4", + "20260629_123127_test_upload_image.png", + "20260629_123101_test_upload_image.png", "20260629_120459_fs_bitch insemination_dup_20260626_003125_dup_Pasted image (9).png.mp4", - "20260624_154536_img_72.png", "20260629_114451_sc_sc_image.nobg.png", "20260629_114346_sc_sc_image.nobg.png", "_turntable/cg_4e2b6449/views/view_023_345deg.png", @@ -5710,6 +5715,7 @@ "20260624_154712_img_72.png", "20260624_154639_img_72.png", "20260624_154548_img_72.png", + "20260624_154536_img_72.png", "20260624_154525_img_72.png", "20260624_154514_img_72.png", "20260624_154503_img_72.png", @@ -12806,7 +12812,8 @@ } if (!data) { try { - const r = await fetch(`${API}/videos`); + const url = forceFresh ? `${API}/videos?refresh=true` : `${API}/videos`; + const r = await fetch(url); if (r.ok) { data = await r.json(); } } catch (_) {} } @@ -12871,16 +12878,89 @@ } // Template grid view - let html = '
Template videos
'; - if (!availableVideos.length) { + let html = ` +
+
Template videos
+ +
`; + + if (!wireframeGroups || !wireframeGroups.length) { html += '
No wireframe videos found
'; } else { - html += '
'; - availableVideos.forEach(v => { - const vSafe = v.replace(/'/g, "\\'"); - const trimBtn = ``; - html += _tplCardHTML(v, _fsSelectedVideo === v, `sbSelectTemplate('${vSafe}')`, trimBtn); + html += '
'; + if (!window._expandedVideoGroups) { + window._expandedVideoGroups = {}; + } + wireframeGroups.forEach(g => { + const hasClips = g.clips && g.clips.length > 0; + const repVideo = g.video || (hasClips ? g.clips[0] : null); + if (!repVideo) return; + + const isExpanded = !!window._expandedVideoGroups[g.stem]; + const repVideoSafe = repVideo.replace(/'/g, "\\'"); + const isSelected = _fsSelectedVideo === repVideo || (g.video && _fsSelectedVideo === g.video); + + const selectedClip = hasClips ? g.clips.find(clip => _fsSelectedVideo === clip) : null; + const rowBorder = (isSelected || selectedClip) ? 'border-color:#6d28d9;background:#1e1b4b' : 'border-color:#27272a;background:#09090b'; + + html += ` +
+
+
+
+ +
+
+
+
+ 📹 ${escHtml(g.stem)} +
+
+ ${g.video ? 'Full video' : 'Clips only'} + ${selectedClip ? `· Selected clip: ${escHtml(selectedClip.replace(g.stem + '_', '').replace('.mp4', ''))}` : ''} + ${isSelected && !selectedClip ? `· Selected` : ''} +
+
+
+ ${g.video ? `` : ''} + ${hasClips ? ` + + ` : ''} +
+
`; + + if (hasClips && isExpanded) { + html += ` +
+
Sub-Clips
+
`; + + g.clips.forEach(clip => { + const cSafe = clip.replace(/'/g, "\\'"); + const isClipSelected = _fsSelectedVideo === clip; + const clipLabel = clip.replace(g.stem + '_', '').replace('.mp4', ''); + const pillBg = isClipSelected ? 'background:#6d28d9;color:#fff;border-color:#8b5cf6' : 'background:#18181b;color:#a1a1aa;border-color:#27272a'; + + html += ` + `; + }); + + html += ` +
+
`; + } + + html += `
`; }); html += '
'; } @@ -12937,6 +13017,28 @@ renderSidebarFaceswap(); } + function toggleGroupClips(stem) { + if (!window._expandedVideoGroups) window._expandedVideoGroups = {}; + window._expandedVideoGroups[stem] = !window._expandedVideoGroups[stem]; + if (_activeSidebarTab === 'faceswap') { + renderSidebarFaceswap(); + } else if (_activeSidebarTab === 'scenery') { + renderSidebarScenery(); + } + } + + async function refreshTemplateVideos(event) { + if (event) event.stopPropagation(); + showToast("Refreshing video library...", "info"); + await loadTemplateVideos(true); + if (_activeSidebarTab === 'faceswap') { + renderSidebarFaceswap(); + } else if (_activeSidebarTab === 'scenery') { + renderSidebarScenery(); + } + showToast("Video library refreshed", "success"); + } + function openTrimPanel(videoName) { _fsTrimVideo = videoName; renderSidebarFaceswap(); @@ -13275,56 +13377,118 @@ // --- Video picker grid (collapsible) --- if (!_sceneVideo || gridOpen) { - html += '
Background source library
'; + html += ` +
+
Background source library
+ +
`; + if (wireframeGroups && wireframeGroups.length) { - html += '
'; + html += '
'; + if (!window._expandedVideoGroups) { + window._expandedVideoGroups = {}; + } wireframeGroups.forEach(g => { - const hasClips = g.clips && g.clips.length; - const hasFrames = g.frames && g.frames.length; - + const hasClips = g.clips && g.clips.length > 0; + const hasFrames = g.frames && g.frames.length > 0; + const repVideo = g.video || (hasClips ? g.clips[0] : null); + if (!repVideo) return; + + const isExpanded = !!window._expandedVideoGroups[g.stem]; + const repVideoSafe = repVideo.replace(/'/g, "\\'"); + const isSelected = _sceneVideo === repVideo || (g.video && _sceneVideo === g.video); + + const selectedClip = hasClips ? g.clips.find(clip => _sceneVideo === clip) : null; + const selectedFrame = hasFrames ? g.frames.find(frame => _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame) : null; + const rowBorder = (isSelected || selectedClip || selectedFrame) ? 'border-color:#6d28d9;background:#1e1b4b' : 'border-color:#27272a;background:#09090b'; + html += ` -
-
- 📹 ${escHtml(g.stem)} - ${g.video ? 'source video' : 'no source'} +
+
+
+
+ +
+
+
+
+ 📹 ${escHtml(g.stem)} +
+
+ ${g.video ? 'Full video' : 'Clips only'} + ${selectedClip ? `· Selected clip: ${escHtml(selectedClip.replace(g.stem + '_', '').replace('.mp4', ''))}` : ''} + ${selectedFrame ? `· Selected frame` : ''} + ${isSelected && !selectedClip ? `· Selected` : ''} +
+
+
+ ${(hasClips || hasFrames) ? ` + + ` : ''} +
`; - - // Render main video if exists - if (g.video) { - html += '
'; - const vSafe = g.video.replace(/'/g, "\\'"); - html += _tplCardHTML(g.video, _sceneVideo === g.video, `sceneSelectVideo('${vSafe}')`); - html += '
'; - } - - // Render trimmed clips if they exist - if (hasClips) { - html += '
✂ Trimmed Clips:
'; - html += '
'; - g.clips.forEach(clip => { - const cSafe = clip.replace(/'/g, "\\'"); - html += _tplCardHTML(clip, _sceneVideo === clip, `sceneSelectVideo('${cSafe}')`); - }); - html += '
'; - } - - // Render extracted frames if they exist - if (hasFrames) { - html += '
🖼 Extracted Frames:
'; - html += '
'; - g.frames.forEach(frame => { - const fSafe = frame.replace(/'/g, "\\'"); - const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame; - const borderStyle = isSelected ? 'border:2px solid #2563eb' : 'border:1px solid #333'; + + if ((hasClips || hasFrames) && isExpanded) { + html += ` +
`; + + if (hasClips) { html += ` -
- +
+
Sub-Clips
+
`; + + g.clips.forEach(clip => { + const cSafe = clip.replace(/'/g, "\\'"); + const isClipSelected = _sceneVideo === clip; + const clipLabel = clip.replace(g.stem + '_', '').replace('.mp4', ''); + const pillBg = isClipSelected ? 'background:#6d28d9;color:#fff;border-color:#8b5cf6' : 'background:#18181b;color:#a1a1aa;border-color:#27272a'; + + html += ` + `; + }); + + html += ` +
`; - }); - html += '
'; + } + + if (hasFrames) { + html += ` +
+
Extracted Frames
+
`; + + g.frames.forEach(frame => { + const fSafe = frame.replace(/'/g, "\\'"); + const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame; + const borderStyle = isSelected ? 'border:2px solid #8b5cf6' : 'border:1px solid #27272a'; + html += ` +
+ +
`; + }); + + html += ` +
+
`; + } + + html += ` +
`; } - - html += '
'; + + html += `
`; }); html += '
'; } else if (availableVideos.length) { diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py index 31643fd..9054845 100644 --- a/tour-comfy/edit_api.py +++ b/tour-comfy/edit_api.py @@ -2452,19 +2452,28 @@ def _get_grouped_wireframes(wireframe_dir: str) -> dict: } +def _update_static_videos() -> dict: + output_dir = _load_output_dir() + data_dir = os.path.join(output_dir, "_data") + os.makedirs(data_dir, exist_ok=True) + wireframe_dir = _load_wireframe_dir() + data = _get_grouped_wireframes(wireframe_dir) + _write_json(os.path.join(data_dir, "videos.json"), data) + return data + + @app.get("/videos") -def list_videos(): +def list_videos(refresh: bool = False): """Return available wireframe template videos and grouped wireframe assets.""" output_dir = _load_output_dir() static_file = os.path.join(output_dir, "_data", "videos.json") - if os.path.exists(static_file): + if not refresh and os.path.exists(static_file): try: with open(static_file, "r") as f: return json.load(f) except Exception as static_err: print(f"[static-get] Failed to load videos.json: {static_err}") - wireframe_dir = _load_wireframe_dir() - return _get_grouped_wireframes(wireframe_dir) + return _update_static_videos() @app.get("/wireframe/frame/{video_name}") @@ -2557,6 +2566,11 @@ def trim_wireframe(req: TrimRequest): if r.returncode != 0: raise HTTPException(500, f"ffmpeg error: {r.stderr.decode(errors='replace')[:500]}") + try: + _update_static_videos() + except Exception as update_err: + print(f"[trim] Failed to update static videos: {update_err}") + return {'output_name': out_name, 'start': req.start, 'end': req.end} diff --git a/tour-comfy/test_regression_api.py b/tour-comfy/test_regression_api.py index f662c4e..ec3c60d 100644 --- a/tour-comfy/test_regression_api.py +++ b/tour-comfy/test_regression_api.py @@ -599,5 +599,29 @@ class TestAPIRegression(unittest.TestCase): self.assertIn("test_ref_image_123.png", _failed_backfill_filenames) + def test_11_video_listing_refresh_and_trim_synchronization(self): + from unittest.mock import patch + import edit_api + + # Mock wireframe folder and output directories + with patch("edit_api._load_wireframe_dir", return_value="/tmp/test_wireframe"), \ + patch("edit_api._load_output_dir", return_value=self.output_dir), \ + patch("os.path.isdir", return_value=True), \ + patch("os.listdir", return_value=["dance.mp4", "dance_10s-20s.mp4", "notes.txt"]): + + # Verify that calling /videos?refresh=true triggers grouping correctly + response = self.client.get("/videos?refresh=true") + self.assertEqual(response.status_code, 200) + data = response.json() + + # Grouping checks + self.assertIn("groups", data) + groups = data["groups"] + # Find group with stem "dance" + dance_grp = next((g for g in groups if g["stem"] == "dance"), None) + self.assertIsNotNone(dance_grp) + self.assertEqual(dance_grp["video"], "dance.mp4") + self.assertIn("dance_10s-20s.mp4", dance_grp["clips"]) + if __name__ == "__main__": unittest.main()