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
+
🔄 Refresh
+
`;
+
+ 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 ? `
+
+ Clips (${g.clips.length})
+ ${isExpanded ? 'â–²' : 'â–¼'}
+
+ ` : ''}
+
+
`;
+
+ 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 += `
+
+ ${escHtml(clipLabel)}
+ `;
+ });
+
+ 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
+
🔄 Refresh
+
`;
+
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) ? `
+
+ Clips/Frames
+ ${isExpanded ? 'â–²' : 'â–¼'}
+
+ ` : ''}
+
`;
-
- // 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 += `
+
+ ${escHtml(clipLabel)}
+ `;
+ });
+
+ 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()