updates UI
This commit is contained in:
@@ -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 = '<div class="sb-label">Template videos</div>';
|
||||
if (!availableVideos.length) {
|
||||
let html = `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
||||
<div class="sb-label" style="margin:0">Template videos</div>
|
||||
<button class="sb-btn" onclick="refreshTemplateVideos(event)" style="font-size:10px;padding:2px 6px;line-height:1">🔄 Refresh</button>
|
||||
</div>`;
|
||||
|
||||
if (!wireframeGroups || !wireframeGroups.length) {
|
||||
html += '<div style="font-size:11px;color:#555;padding:6px 0">No wireframe videos found</div>';
|
||||
} else {
|
||||
html += '<div class="sb-template-grid">';
|
||||
availableVideos.forEach(v => {
|
||||
const vSafe = v.replace(/'/g, "\\'");
|
||||
const trimBtn = `<button class="sb-template-trim-btn" title="Trim video"
|
||||
onclick="event.stopPropagation();openTrimPanel('${vSafe}')">✂</button>`;
|
||||
html += _tplCardHTML(v, _fsSelectedVideo === v, `sbSelectTemplate('${vSafe}')`, trimBtn);
|
||||
html += '<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">';
|
||||
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 += `
|
||||
<div style="border:1px solid;border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:8px;transition:all 0.15s;${rowBorder}">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div style="width:70px;height:50px;flex-shrink:0;position:relative;border-radius:4px;overflow:hidden;background:#000">
|
||||
<div class="sb-template-card compact-card ${isSelected && !selectedClip ? 'selected' : ''}"
|
||||
onclick="sbSelectTemplate('${repVideoSafe}')"
|
||||
onmouseenter="_tplPlay(this,'${repVideoSafe}')" onmouseleave="_tplStop(this)"
|
||||
style="width:100%;height:100%;margin:0;border:none">
|
||||
<img src="${API}/wireframe/frame/${encodeURIComponent(repVideo)}?t=0" loading="lazy" style="width:100%;height:100%;object-fit:cover" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-grow:1;min-width:0">
|
||||
<div style="font-size:12px;font-weight:bold;color:#f4f4f5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${escHtml(g.stem)}">
|
||||
📹 ${escHtml(g.stem)}
|
||||
</div>
|
||||
<div style="font-size:10px;color:#71717a;margin-top:2px;display:flex;align-items:center;gap:4px">
|
||||
<span>${g.video ? 'Full video' : 'Clips only'}</span>
|
||||
${selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected clip: ${escHtml(selectedClip.replace(g.stem + '_', '').replace('.mp4', ''))}</span>` : ''}
|
||||
${isSelected && !selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
|
||||
${g.video ? `<button class="sb-template-trim-btn" title="Trim video" onclick="event.stopPropagation();openTrimPanel('${repVideoSafe}')" style="position:static;width:24px;height:24px;line-height:24px;font-size:11px;padding:0">✂</button>` : ''}
|
||||
${hasClips ? `
|
||||
<button class="sb-btn" onclick="event.stopPropagation();toggleGroupClips('${g.stem.replace(/'/g, "\\'")}')"
|
||||
style="font-size:10px;padding:3px 6px;line-height:1;background:#27272a;color:#d4d4d8;border:1px solid #3f3f46;display:flex;align-items:center;gap:2px">
|
||||
<span>Clips (${g.clips.length})</span>
|
||||
<span>${isExpanded ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (hasClips && isExpanded) {
|
||||
html += `
|
||||
<div style="border-top:1px solid #27272a;padding-top:6px;margin-top:2px">
|
||||
<div style="font-size:9px;color:#71717a;text-transform:uppercase;font-weight:bold;letter-spacing:0.5px;margin-bottom:4px">Sub-Clips</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px">`;
|
||||
|
||||
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 += `
|
||||
<button class="sb-btn" onclick="sbSelectTemplate('${cSafe}')"
|
||||
style="font-size:10px;padding:2px 6px;border-radius:3px;border:1px solid;cursor:pointer;transition:all 0.1s;${pillBg}">
|
||||
${escHtml(clipLabel)}
|
||||
</button>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
@@ -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 += '<div class="sb-label">Background source library</div>';
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
||||
<div class="sb-label" style="margin:0">Background source library</div>
|
||||
<button class="sb-btn" onclick="refreshTemplateVideos(event)" style="font-size:10px;padding:2px 6px;line-height:1">🔄 Refresh</button>
|
||||
</div>`;
|
||||
|
||||
if (wireframeGroups && wireframeGroups.length) {
|
||||
html += '<div style="display:flex;flex-direction:column;gap:12px;margin-bottom:12px">';
|
||||
html += '<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">';
|
||||
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 += `
|
||||
<div style="background:#141414;border:1px solid #222;border-radius:6px;padding:8px">
|
||||
<div style="font-size:12px;font-weight:bold;color:#ccc;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between">
|
||||
<span>📹 ${escHtml(g.stem)}</span>
|
||||
<span style="font-size:10px;color:#666">${g.video ? 'source video' : 'no source'}</span>
|
||||
<div style="border:1px solid;border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:8px;transition:all 0.15s;${rowBorder}">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div style="width:70px;height:50px;flex-shrink:0;position:relative;border-radius:4px;overflow:hidden;background:#000">
|
||||
<div class="sb-template-card compact-card ${isSelected && !selectedClip ? 'selected' : ''}"
|
||||
onclick="sceneSelectVideo('${repVideoSafe}')"
|
||||
onmouseenter="_tplPlay(this,'${repVideoSafe}')" onmouseleave="_tplStop(this)"
|
||||
style="width:100%;height:100%;margin:0;border:none">
|
||||
<img src="${API}/wireframe/frame/${encodeURIComponent(repVideo)}?t=0" loading="lazy" style="width:100%;height:100%;object-fit:cover" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-grow:1;min-width:0">
|
||||
<div style="font-size:12px;font-weight:bold;color:#f4f4f5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${escHtml(g.stem)}">
|
||||
📹 ${escHtml(g.stem)}
|
||||
</div>
|
||||
<div style="font-size:10px;color:#71717a;margin-top:2px;display:flex;align-items:center;gap:4px">
|
||||
<span>${g.video ? 'Full video' : 'Clips only'}</span>
|
||||
${selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected clip: ${escHtml(selectedClip.replace(g.stem + '_', '').replace('.mp4', ''))}</span>` : ''}
|
||||
${selectedFrame ? `<span style="color:#a78bfa;font-weight:bold">· Selected frame</span>` : ''}
|
||||
${isSelected && !selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
|
||||
${(hasClips || hasFrames) ? `
|
||||
<button class="sb-btn" onclick="event.stopPropagation();toggleGroupClips('${g.stem.replace(/'/g, "\\'")}')"
|
||||
style="font-size:10px;padding:3px 6px;line-height:1;background:#27272a;color:#d4d4d8;border:1px solid #3f3f46;display:flex;align-items:center;gap:2px">
|
||||
<span>Clips/Frames</span>
|
||||
<span>${isExpanded ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Render main video if exists
|
||||
if (g.video) {
|
||||
html += '<div class="sb-template-grid" style="margin-bottom:6px">';
|
||||
const vSafe = g.video.replace(/'/g, "\\'");
|
||||
html += _tplCardHTML(g.video, _sceneVideo === g.video, `sceneSelectVideo('${vSafe}')`);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Render trimmed clips if they exist
|
||||
if (hasClips) {
|
||||
html += '<div style="font-size:10px;color:#888;margin:4px 0 2px">✂ Trimmed Clips:</div>';
|
||||
html += '<div class="sb-template-grid" style="margin-bottom:6px">';
|
||||
g.clips.forEach(clip => {
|
||||
const cSafe = clip.replace(/'/g, "\\'");
|
||||
html += _tplCardHTML(clip, _sceneVideo === clip, `sceneSelectVideo('${cSafe}')`);
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Render extracted frames if they exist
|
||||
if (hasFrames) {
|
||||
html += '<div style="font-size:10px;color:#888;margin:4px 0 2px">🖼 Extracted Frames:</div>';
|
||||
html += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
|
||||
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 += `
|
||||
<div style="border-top:1px solid #27272a;padding-top:6px;margin-top:2px;display:flex;flex-direction:column;gap:6px">`;
|
||||
|
||||
if (hasClips) {
|
||||
html += `
|
||||
<div style="cursor:pointer;position:relative" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
|
||||
<img src="${API}/wireframe/${encodeURIComponent(frame)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}">
|
||||
<div>
|
||||
<div style="font-size:9px;color:#71717a;text-transform:uppercase;font-weight:bold;letter-spacing:0.5px;margin-bottom:4px">Sub-Clips</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px">`;
|
||||
|
||||
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 += `
|
||||
<button class="sb-btn" onclick="sceneSelectVideo('${cSafe}')"
|
||||
style="font-size:10px;padding:2px 6px;border-radius:3px;border:1px solid;cursor:pointer;transition:all 0.1s;${pillBg}">
|
||||
${escHtml(clipLabel)}
|
||||
</button>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (hasFrames) {
|
||||
html += `
|
||||
<div>
|
||||
<div style="font-size:9px;color:#71717a;text-transform:uppercase;font-weight:bold;letter-spacing:0.5px;margin-bottom:4px">Extracted Frames</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px">`;
|
||||
|
||||
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 += `
|
||||
<div style="cursor:pointer;position:relative" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
|
||||
<img src="${API}/wireframe/${encodeURIComponent(frame)}" style="width:40px;height:40px;object-fit:cover;border-radius:4px;${borderStyle}">
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
} else if (availableVideos.length) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user