updates UI
This commit is contained in:
@@ -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
|
- 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
|
- backfill, people-count, 21+, IS_COMPLETE, RESTRAINT
|
||||||
- the wireframe / videos / sub-clips worden niet direct weergegeven
|
- 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
|
## refine
|
||||||
|
|
||||||
- when refresh page, we lose track of current jobs running.
|
- when refresh page, we lose track of current jobs running.
|
||||||
|
|||||||
@@ -2334,8 +2334,13 @@
|
|||||||
|
|
||||||
// --- HYDRATION_START ---
|
// --- HYDRATION_START ---
|
||||||
const PRELOADED_IMAGES = [
|
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",
|
"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_114451_sc_sc_image.nobg.png",
|
||||||
"20260629_114346_sc_sc_image.nobg.png",
|
"20260629_114346_sc_sc_image.nobg.png",
|
||||||
"_turntable/cg_4e2b6449/views/view_023_345deg.png",
|
"_turntable/cg_4e2b6449/views/view_023_345deg.png",
|
||||||
@@ -5710,6 +5715,7 @@
|
|||||||
"20260624_154712_img_72.png",
|
"20260624_154712_img_72.png",
|
||||||
"20260624_154639_img_72.png",
|
"20260624_154639_img_72.png",
|
||||||
"20260624_154548_img_72.png",
|
"20260624_154548_img_72.png",
|
||||||
|
"20260624_154536_img_72.png",
|
||||||
"20260624_154525_img_72.png",
|
"20260624_154525_img_72.png",
|
||||||
"20260624_154514_img_72.png",
|
"20260624_154514_img_72.png",
|
||||||
"20260624_154503_img_72.png",
|
"20260624_154503_img_72.png",
|
||||||
@@ -12806,7 +12812,8 @@
|
|||||||
}
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
try {
|
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(); }
|
if (r.ok) { data = await r.json(); }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -12871,16 +12878,89 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Template grid view
|
// Template grid view
|
||||||
let html = '<div class="sb-label">Template videos</div>';
|
let html = `
|
||||||
if (!availableVideos.length) {
|
<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>';
|
html += '<div style="font-size:11px;color:#555;padding:6px 0">No wireframe videos found</div>';
|
||||||
} else {
|
} else {
|
||||||
html += '<div class="sb-template-grid">';
|
html += '<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">';
|
||||||
availableVideos.forEach(v => {
|
if (!window._expandedVideoGroups) {
|
||||||
const vSafe = v.replace(/'/g, "\\'");
|
window._expandedVideoGroups = {};
|
||||||
const trimBtn = `<button class="sb-template-trim-btn" title="Trim video"
|
}
|
||||||
onclick="event.stopPropagation();openTrimPanel('${vSafe}')">✂</button>`;
|
wireframeGroups.forEach(g => {
|
||||||
html += _tplCardHTML(v, _fsSelectedVideo === v, `sbSelectTemplate('${vSafe}')`, trimBtn);
|
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>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
@@ -12937,6 +13017,28 @@
|
|||||||
renderSidebarFaceswap();
|
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) {
|
function openTrimPanel(videoName) {
|
||||||
_fsTrimVideo = videoName;
|
_fsTrimVideo = videoName;
|
||||||
renderSidebarFaceswap();
|
renderSidebarFaceswap();
|
||||||
@@ -13275,56 +13377,118 @@
|
|||||||
|
|
||||||
// --- Video picker grid (collapsible) ---
|
// --- Video picker grid (collapsible) ---
|
||||||
if (!_sceneVideo || gridOpen) {
|
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) {
|
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 => {
|
wireframeGroups.forEach(g => {
|
||||||
const hasClips = g.clips && g.clips.length;
|
const hasClips = g.clips && g.clips.length > 0;
|
||||||
const hasFrames = g.frames && g.frames.length;
|
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 += `
|
html += `
|
||||||
<div style="background:#141414;border:1px solid #222;border-radius:6px;padding:8px">
|
<div style="border:1px solid;border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:8px;transition:all 0.15s;${rowBorder}">
|
||||||
<div style="font-size:12px;font-weight:bold;color:#ccc;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between">
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
<span>📹 ${escHtml(g.stem)}</span>
|
<div style="width:70px;height:50px;flex-shrink:0;position:relative;border-radius:4px;overflow:hidden;background:#000">
|
||||||
<span style="font-size:10px;color:#666">${g.video ? 'source video' : 'no source'}</span>
|
<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>`;
|
</div>`;
|
||||||
|
|
||||||
// Render main video if exists
|
if ((hasClips || hasFrames) && isExpanded) {
|
||||||
if (g.video) {
|
html += `
|
||||||
html += '<div class="sb-template-grid" style="margin-bottom:6px">';
|
<div style="border-top:1px solid #27272a;padding-top:6px;margin-top:2px;display:flex;flex-direction:column;gap:6px">`;
|
||||||
const vSafe = g.video.replace(/'/g, "\\'");
|
|
||||||
html += _tplCardHTML(g.video, _sceneVideo === g.video, `sceneSelectVideo('${vSafe}')`);
|
if (hasClips) {
|
||||||
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';
|
|
||||||
html += `
|
html += `
|
||||||
<div style="cursor:pointer;position:relative" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
|
<div>
|
||||||
<img src="${API}/wireframe/${encodeURIComponent(frame)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}">
|
<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>`;
|
</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>';
|
html += '</div>';
|
||||||
} else if (availableVideos.length) {
|
} 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")
|
@app.get("/videos")
|
||||||
def list_videos():
|
def list_videos(refresh: bool = False):
|
||||||
"""Return available wireframe template videos and grouped wireframe assets."""
|
"""Return available wireframe template videos and grouped wireframe assets."""
|
||||||
output_dir = _load_output_dir()
|
output_dir = _load_output_dir()
|
||||||
static_file = os.path.join(output_dir, "_data", "videos.json")
|
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:
|
try:
|
||||||
with open(static_file, "r") as f:
|
with open(static_file, "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as static_err:
|
except Exception as static_err:
|
||||||
print(f"[static-get] Failed to load videos.json: {static_err}")
|
print(f"[static-get] Failed to load videos.json: {static_err}")
|
||||||
wireframe_dir = _load_wireframe_dir()
|
return _update_static_videos()
|
||||||
return _get_grouped_wireframes(wireframe_dir)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/wireframe/frame/{video_name}")
|
@app.get("/wireframe/frame/{video_name}")
|
||||||
@@ -2557,6 +2566,11 @@ def trim_wireframe(req: TrimRequest):
|
|||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
raise HTTPException(500, f"ffmpeg error: {r.stderr.decode(errors='replace')[:500]}")
|
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}
|
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)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user