reorder
This commit is contained in:
@@ -2305,6 +2305,8 @@
|
|||||||
|
|
||||||
// --- HYDRATION_START ---
|
// --- HYDRATION_START ---
|
||||||
const PRELOADED_IMAGES = [
|
const PRELOADED_IMAGES = [
|
||||||
|
"20260628_045734_image.png",
|
||||||
|
"_turntable/cg_7ec17537/views/20260628_045628_sc_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.nobg.png",
|
||||||
"_turntable/cg_7ec17537/views/20260628_044252_sc_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.nobg.png",
|
"_turntable/cg_7ec17537/views/20260628_044252_sc_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.nobg.png",
|
||||||
"_turntable/cg_7ec17537/views/20260628_043545_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
|
"_turntable/cg_7ec17537/views/20260628_043545_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
|
||||||
"_turntable/cg_7ec17537/views/20260628_043148_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.nobg.png",
|
"_turntable/cg_7ec17537/views/20260628_043148_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.nobg.png",
|
||||||
@@ -5129,9 +5131,9 @@
|
|||||||
"20260624_183010_0_20260624_182958_image.png",
|
"20260624_183010_0_20260624_182958_image.png",
|
||||||
"20260624_182958_image.png",
|
"20260624_182958_image.png",
|
||||||
"20260624_174900_image.png",
|
"20260624_174900_image.png",
|
||||||
|
"20260624_174943_dup_20260624_173115_image.png",
|
||||||
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
|
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
|
||||||
"20260624_173115_image.png",
|
"20260624_173115_image.png",
|
||||||
"20260624_174943_dup_20260624_173115_image.png",
|
|
||||||
"20260624_174757_image.png",
|
"20260624_174757_image.png",
|
||||||
"20260624_174442_image.png",
|
"20260624_174442_image.png",
|
||||||
"20260624_172923_image.nobg.png",
|
"20260624_172923_image.nobg.png",
|
||||||
@@ -6923,6 +6925,8 @@
|
|||||||
let _fsJobId = null;
|
let _fsJobId = null;
|
||||||
let _fsJobPollTimer = null;
|
let _fsJobPollTimer = null;
|
||||||
let availableVideos = []; // populated from /videos
|
let availableVideos = []; // populated from /videos
|
||||||
|
let wireframeGroups = []; // grouped wireframe assets
|
||||||
|
let wireframeStandaloneImages = []; // standalone / uploaded wireframe images
|
||||||
let _fsActiveTab = 'swap';
|
let _fsActiveTab = 'swap';
|
||||||
let _fsSelectedPoses = new Set();
|
let _fsSelectedPoses = new Set();
|
||||||
let _sbPoseFilter = ''; // live pose-name filter query (Generate tab)
|
let _sbPoseFilter = ''; // live pose-name filter query (Generate tab)
|
||||||
@@ -11640,15 +11644,26 @@
|
|||||||
// ---- faceswap sidebar tab ----
|
// ---- faceswap sidebar tab ----
|
||||||
|
|
||||||
async function loadTemplateVideos() {
|
async function loadTemplateVideos() {
|
||||||
|
let data = null;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${API}/output/_data/videos.json`, { signal: AbortSignal.timeout(3000) });
|
const r = await fetch(`${API}/output/_data/videos.json`, { signal: AbortSignal.timeout(3000) });
|
||||||
if (r.ok) { availableVideos = (await r.json()).videos || []; return; }
|
if (r.ok) { data = await r.json(); }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
try {
|
if (!data) {
|
||||||
const r = await fetch(`${API}/videos`);
|
try {
|
||||||
if (!r.ok) return;
|
const r = await fetch(`${API}/videos`);
|
||||||
availableVideos = (await r.json()).videos || [];
|
if (r.ok) { data = await r.json(); }
|
||||||
} catch (_) { availableVideos = []; }
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
availableVideos = data.videos || [];
|
||||||
|
wireframeGroups = data.groups || [];
|
||||||
|
wireframeStandaloneImages = data.standalone_images || [];
|
||||||
|
} else {
|
||||||
|
availableVideos = [];
|
||||||
|
wireframeGroups = [];
|
||||||
|
wireframeStandaloneImages = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function lbFaceswap() {
|
async function lbFaceswap() {
|
||||||
@@ -12031,8 +12046,59 @@
|
|||||||
|
|
||||||
// --- Video picker grid (collapsible) ---
|
// --- Video picker grid (collapsible) ---
|
||||||
if (!_sceneVideo || gridOpen) {
|
if (!_sceneVideo || gridOpen) {
|
||||||
html += '<div class="sb-label">Background video</div>';
|
html += '<div class="sb-label">Background source library</div>';
|
||||||
if (availableVideos.length) {
|
if (wireframeGroups && wireframeGroups.length) {
|
||||||
|
html += '<div style="display:flex;flex-direction:column;gap:12px;margin-bottom:12px">';
|
||||||
|
wireframeGroups.forEach(g => {
|
||||||
|
const hasClips = g.clips && g.clips.length;
|
||||||
|
const hasFrames = g.frames && g.frames.length;
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
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>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
} else if (availableVideos.length) {
|
||||||
html += '<div class="sb-template-grid">';
|
html += '<div class="sb-template-grid">';
|
||||||
availableVideos.forEach(v => {
|
availableVideos.forEach(v => {
|
||||||
const vSafe = v.replace(/'/g, "\\'");
|
const vSafe = v.replace(/'/g, "\\'");
|
||||||
@@ -12042,6 +12108,22 @@
|
|||||||
} else {
|
} else {
|
||||||
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>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standalone / uploaded images section
|
||||||
|
if (wireframeStandaloneImages && wireframeStandaloneImages.length) {
|
||||||
|
html += '<div class="sb-label" style="margin-top:10px">Standalone / Uploaded Images</div>';
|
||||||
|
html += '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;background:#141414;border:1px solid #222;border-radius:6px;padding:8px">';
|
||||||
|
wireframeStandaloneImages.forEach(img => {
|
||||||
|
const imgSafe = img.replace(/'/g, "\\'");
|
||||||
|
const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === img;
|
||||||
|
const borderStyle = isSelected ? 'border:2px solid #2563eb' : 'border:1px solid #333';
|
||||||
|
html += `
|
||||||
|
<div style="cursor:pointer;position:relative" title="${escHtml(img)}" onclick="sceneSelectWireframeImage('${imgSafe}')">
|
||||||
|
<img src="${API}/wireframe/${encodeURIComponent(img)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}">
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload alternative for the background (still image 1)
|
// Upload alternative for the background (still image 1)
|
||||||
@@ -12200,6 +12282,7 @@
|
|||||||
function _sceneThumb(ref) {
|
function _sceneThumb(ref) {
|
||||||
if (!ref) return '';
|
if (!ref) return '';
|
||||||
if (ref.kind === 'bytes') return 'data:image/png;base64,' + ref.data;
|
if (ref.kind === 'bytes') return 'data:image/png;base64,' + ref.data;
|
||||||
|
if (ref.kind === 'wireframe') return `${API}/wireframe/${encodeURIComponent(ref.name)}`;
|
||||||
const idx = lbNames.indexOf(ref.name);
|
const idx = lbNames.indexOf(ref.name);
|
||||||
const u = idx >= 0 ? lbUrls[idx] : (IMAGE_FOLDER + ref.name);
|
const u = idx >= 0 ? lbUrls[idx] : (IMAGE_FOLDER + ref.name);
|
||||||
return isVideo(ref.name) ? posterFor(u) : u;
|
return isVideo(ref.name) ? posterFor(u) : u;
|
||||||
@@ -12286,14 +12369,23 @@
|
|||||||
|
|
||||||
async function sceneSelectVideo(v) {
|
async function sceneSelectVideo(v) {
|
||||||
_sceneVideo = v; _sceneFrameBytes = null;
|
_sceneVideo = v; _sceneFrameBytes = null;
|
||||||
if (_sceneRefs[0] && _sceneRefs[0].kind === 'bytes') _sceneRefs[0] = null;
|
if (_sceneRefs[0] && (_sceneRefs[0].kind === 'bytes' || _sceneRefs[0].kind === 'wireframe')) _sceneRefs[0] = null;
|
||||||
_sceneGridOpen = false; // collapse the picker so the player is front-and-centre
|
_sceneGridOpen = false; // collapse the picker so the player is front-and-centre
|
||||||
renderSidebarScenery();
|
renderSidebarScenery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sceneSelectWireframeImage(filename) {
|
||||||
|
_sceneRefs[0] = { kind: 'wireframe', name: filename };
|
||||||
|
_sceneVideo = null;
|
||||||
|
_sceneFrameBytes = null;
|
||||||
|
_sceneGridOpen = false;
|
||||||
|
_sceneRefresh();
|
||||||
|
showToast('Background set from wireframe folder', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
function sceneReleaseFrame() {
|
function sceneReleaseFrame() {
|
||||||
_sceneFrameBytes = null;
|
_sceneFrameBytes = null;
|
||||||
if (_sceneRefs[0] && _sceneRefs[0].kind === 'bytes') _sceneRefs[0] = null;
|
if (_sceneRefs[0] && (_sceneRefs[0].kind === 'bytes' || _sceneRefs[0].kind === 'wireframe')) _sceneRefs[0] = null;
|
||||||
renderSidebarScenery(); // re-render restores the live video + slider + capture button
|
renderSidebarScenery(); // re-render restores the live video + slider + capture button
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12315,8 +12407,10 @@
|
|||||||
} else {
|
} else {
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
_sceneFrameBytes = d.frame_b64;
|
_sceneFrameBytes = d.frame_b64;
|
||||||
_sceneRefs[0] = { kind: 'bytes', data: d.frame_b64 }; // background → slot 0
|
_sceneRefs[0] = { kind: 'wireframe', name: d.filename }; // background → slot 0
|
||||||
_sceneRefresh(); // show the frozen frame + update slot badge
|
_sceneRefresh(); // show the frozen frame + update slot badge
|
||||||
|
// Reload template list so the new frame is instantly visible in the library
|
||||||
|
loadTemplateVideos().then(() => _sceneRefresh());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Frame extract error: ' + e, 'error');
|
showToast('Frame extract error: ' + e, 'error');
|
||||||
@@ -12324,17 +12418,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sceneHandleUpload(event) {
|
async function sceneHandleUpload(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = e => {
|
showToast('Uploading background image to wireframe folder…', 'info');
|
||||||
_sceneFrameBytes = e.target.result.split(',')[1];
|
const fd = new FormData();
|
||||||
_sceneRefs[0] = { kind: 'bytes', data: _sceneFrameBytes }; // background → slot 0
|
fd.append('image', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${API}/wireframe/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: fd
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
showToast('Upload failed: ' + await r.text(), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = await r.json();
|
||||||
|
showToast('Upload successful!', 'success');
|
||||||
|
|
||||||
|
// Select this uploaded image as background
|
||||||
|
_sceneRefs[0] = { kind: 'wireframe', name: d.filename };
|
||||||
_sceneVideo = null;
|
_sceneVideo = null;
|
||||||
renderSidebarScenery();
|
_sceneFrameBytes = null;
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
// Force a reload of template videos so the newly uploaded file is visible in the library
|
||||||
|
availableVideos = [];
|
||||||
|
await loadTemplateVideos();
|
||||||
|
_sceneRefresh();
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Upload error: ' + e, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitGenerateScenery() {
|
async function submitGenerateScenery() {
|
||||||
@@ -12352,16 +12467,24 @@
|
|||||||
if (textEl) textEl.textContent = 'Submitting…';
|
if (textEl) textEl.textContent = 'Submitting…';
|
||||||
if (prompt) savePromptHistory(prompt);
|
if (prompt) savePromptHistory(prompt);
|
||||||
// slot0 → scene_bytes: bytes directly, or fetch a file-as-background to bytes.
|
// slot0 → scene_bytes: bytes directly, or fetch a file-as-background to bytes.
|
||||||
let sceneBytes;
|
let sceneBytes = null;
|
||||||
try {
|
let sceneImage = null;
|
||||||
sceneBytes = (bg.kind === 'bytes') ? bg.data : await _fetchAsBase64(_sceneUrlForName(bg.name));
|
if (bg.kind === 'bytes') {
|
||||||
} catch (e) {
|
sceneBytes = bg.data;
|
||||||
showToast('Could not read background image: ' + e, 'error');
|
} else if (bg.kind === 'wireframe') {
|
||||||
if (btn) btn.disabled = false; if (statusEl) statusEl.style.display = 'none'; return;
|
sceneImage = bg.name;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
sceneBytes = await _fetchAsBase64(_sceneUrlForName(bg.name));
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Could not read background image: ' + e, 'error');
|
||||||
|
if (btn) btn.disabled = false; if (statusEl) statusEl.style.display = 'none'; return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
model_filename: person.name, // image2 (Picture 2) — person
|
model_filename: person.name, // image2 (Picture 2) — person
|
||||||
scene_bytes: sceneBytes, // image1 (Picture 1) — background
|
scene_bytes: sceneBytes, // image1 (Picture 1) — background
|
||||||
|
scene_image: sceneImage, // image1 (Picture 1) — direct wireframe image filename
|
||||||
scene_video: null,
|
scene_video: null,
|
||||||
scene_time: 0,
|
scene_time: 0,
|
||||||
extra_filename: (extra && extra.kind === 'file') ? extra.name : null, // image3 (Picture 3)
|
extra_filename: (extra && extra.kind === 'file') ? extra.name : null, // image3 (Picture 3)
|
||||||
|
|||||||
@@ -1366,14 +1366,10 @@ def _write_all_static() -> None:
|
|||||||
_write_json(os.path.join(data_dir, "group-names.json"),
|
_write_json(os.path.join(data_dir, "group-names.json"),
|
||||||
database.get_all_group_names())
|
database.get_all_group_names())
|
||||||
|
|
||||||
# videos.json — wireframe template videos
|
# videos.json — wireframe template videos and grouped assets
|
||||||
wireframe_dir = _load_wireframe_dir()
|
wireframe_dir = _load_wireframe_dir()
|
||||||
videos = []
|
|
||||||
if os.path.isdir(wireframe_dir):
|
|
||||||
videos = [f for f in sorted(os.listdir(wireframe_dir))
|
|
||||||
if f.lower().endswith(VIDEO_EXTENSIONS) and not f.startswith('.')]
|
|
||||||
_write_json(os.path.join(data_dir, "videos.json"),
|
_write_json(os.path.join(data_dir, "videos.json"),
|
||||||
{"videos": videos, "wireframe_dir": wireframe_dir})
|
_get_grouped_wireframes(wireframe_dir))
|
||||||
|
|
||||||
# config.json — current config snapshot
|
# config.json — current config snapshot
|
||||||
try:
|
try:
|
||||||
@@ -2001,17 +1997,81 @@ def list_images(archived: bool = False):
|
|||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_grouped_wireframes(wireframe_dir: str) -> dict:
|
||||||
|
"""Scan and return a grouped structure of all videos, clips, and frames in wireframe_dir."""
|
||||||
|
if not os.path.isdir(wireframe_dir):
|
||||||
|
return {"videos": [], "groups": [], "standalone_images": []}
|
||||||
|
|
||||||
|
all_files = sorted(os.listdir(wireframe_dir))
|
||||||
|
videos = [f for f in all_files if f.lower().endswith(VIDEO_EXTENSIONS) and not f.startswith('.')]
|
||||||
|
images = [f for f in all_files if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')) and not f.startswith('.')]
|
||||||
|
|
||||||
|
stem_map = {}
|
||||||
|
|
||||||
|
# Match primary videos and trimmed clips
|
||||||
|
for v in videos:
|
||||||
|
stem = os.path.splitext(v)[0]
|
||||||
|
# Match e.g. dance_10s-20s.mp4
|
||||||
|
m = re.match(r"^(.*)_(\d+)s-(\d+)s$", stem)
|
||||||
|
if m:
|
||||||
|
parent_stem = m.group(1)
|
||||||
|
stem_map.setdefault(parent_stem, {"video": None, "clips": [], "frames": []})
|
||||||
|
stem_map[parent_stem]["clips"].append(v)
|
||||||
|
else:
|
||||||
|
stem_map.setdefault(stem, {"video": None, "clips": [], "frames": []})
|
||||||
|
stem_map[stem]["video"] = v
|
||||||
|
|
||||||
|
# Match extracted frames
|
||||||
|
standalone_images = []
|
||||||
|
for img in images:
|
||||||
|
stem = os.path.splitext(img)[0]
|
||||||
|
# Match e.g. dance-f120.png
|
||||||
|
m = re.match(r"^(.*)-f(\d+)$", stem)
|
||||||
|
if m:
|
||||||
|
parent_stem = m.group(1)
|
||||||
|
stem_map.setdefault(parent_stem, {"video": None, "clips": [], "frames": []})
|
||||||
|
stem_map[parent_stem]["frames"].append(img)
|
||||||
|
else:
|
||||||
|
standalone_images.append(img)
|
||||||
|
|
||||||
|
# Format and sort
|
||||||
|
groups = []
|
||||||
|
for stem, data in sorted(stem_map.items()):
|
||||||
|
# Sort clips by start time if possible
|
||||||
|
def get_clip_start(c):
|
||||||
|
stem_c = os.path.splitext(c)[0]
|
||||||
|
m_c = re.match(r"^.*_(\d+)s-(\d+)s$", stem_c)
|
||||||
|
return int(m_c.group(1)) if m_c else 0
|
||||||
|
|
||||||
|
# Sort frames numerically by frame number
|
||||||
|
def get_frame_num(f):
|
||||||
|
stem_f = os.path.splitext(f)[0]
|
||||||
|
m_f = re.match(r"^.*-f(\d+)$", stem_f)
|
||||||
|
return int(m_f.group(1)) if m_f else 0
|
||||||
|
|
||||||
|
data["clips"].sort(key=get_clip_start)
|
||||||
|
data["frames"].sort(key=get_frame_num)
|
||||||
|
|
||||||
|
groups.append({
|
||||||
|
"stem": stem,
|
||||||
|
"video": data["video"],
|
||||||
|
"clips": data["clips"],
|
||||||
|
"frames": data["frames"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"videos": videos,
|
||||||
|
"groups": groups,
|
||||||
|
"standalone_images": standalone_images,
|
||||||
|
"wireframe_dir": wireframe_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/videos")
|
@app.get("/videos")
|
||||||
def list_videos():
|
def list_videos():
|
||||||
"""Return available wireframe template videos."""
|
"""Return available wireframe template videos and grouped wireframe assets."""
|
||||||
wireframe_dir = _load_wireframe_dir()
|
wireframe_dir = _load_wireframe_dir()
|
||||||
if not os.path.isdir(wireframe_dir):
|
return _get_grouped_wireframes(wireframe_dir)
|
||||||
return {"videos": []}
|
|
||||||
videos = [
|
|
||||||
f for f in sorted(os.listdir(wireframe_dir))
|
|
||||||
if f.lower().endswith(VIDEO_EXTENSIONS) and not f.startswith('.')
|
|
||||||
]
|
|
||||||
return {"videos": videos, "wireframe_dir": wireframe_dir}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/wireframe/frame/{video_name}")
|
@app.get("/wireframe/frame/{video_name}")
|
||||||
@@ -2216,9 +2276,31 @@ def wireframe_extract_frame(req: FrameExtractRequest):
|
|||||||
img = _extract_frame_at(video_path, req.time)
|
img = _extract_frame_at(video_path, req.time)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# Calculate saved filename to return to client
|
||||||
|
try:
|
||||||
|
import cv2
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if cap.isOpened():
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
cap.release()
|
||||||
|
frame_num = int(round(req.time * fps)) if fps > 0 else 0
|
||||||
|
frame_num = max(0, min(total - 1, frame_num))
|
||||||
|
else:
|
||||||
|
frame_num = int(round(req.time * 30.0))
|
||||||
|
except Exception:
|
||||||
|
frame_num = int(round(req.time * 30.0))
|
||||||
|
|
||||||
|
stem = os.path.splitext(req.video_name)[0]
|
||||||
|
saved_filename = f"{stem}-f{frame_num}.png"
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
img.save(buf, format="PNG")
|
img.save(buf, format="PNG")
|
||||||
return {"frame_b64": base64.b64encode(buf.getvalue()).decode()}
|
return {
|
||||||
|
"frame_b64": base64.b64encode(buf.getvalue()).decode(),
|
||||||
|
"filename": saved_filename
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FaceswapRequest(BaseModel):
|
class FaceswapRequest(BaseModel):
|
||||||
@@ -2734,6 +2816,29 @@ def upload_image(
|
|||||||
return {"status": "processing", "filename": filename, "group_id": effective_gid, "prompts": prompt_list}
|
return {"status": "processing", "filename": filename, "group_id": effective_gid, "prompts": prompt_list}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/wireframe/upload")
|
||||||
|
def upload_wireframe_image(
|
||||||
|
image: UploadFile = File(...),
|
||||||
|
):
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
os.makedirs(wireframe_dir, exist_ok=True)
|
||||||
|
|
||||||
|
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||||
|
safe_filename = re.sub(r'[^a-zA-Z0-9_.-]', '_', image.filename or "uploaded")
|
||||||
|
# Ensure extension
|
||||||
|
if not safe_filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
|
||||||
|
safe_filename += ".png"
|
||||||
|
|
||||||
|
filename = f"up_{ts}_{safe_filename}"
|
||||||
|
file_path = os.path.join(wireframe_dir, filename)
|
||||||
|
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
shutil.copyfileobj(image.file, f)
|
||||||
|
|
||||||
|
_invalidate_static()
|
||||||
|
return {"status": "success", "filename": filename}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/edit")
|
@app.post("/edit")
|
||||||
async def edit(
|
async def edit(
|
||||||
image: UploadFile = File(...),
|
image: UploadFile = File(...),
|
||||||
@@ -3381,7 +3486,7 @@ def remove_background_group(group_id: str, background_tasks: BackgroundTasks):
|
|||||||
# --- scenery generation -------------------------------------------------------
|
# --- scenery generation -------------------------------------------------------
|
||||||
|
|
||||||
def _extract_frame_at(video_path: str, t: float) -> Image.Image:
|
def _extract_frame_at(video_path: str, t: float) -> Image.Image:
|
||||||
"""Extract a single frame at time t (seconds) from a video via ffmpeg."""
|
"""Extract a single frame at time t (seconds) from a video via ffmpeg, and save to wireframe dir."""
|
||||||
import subprocess as _sp
|
import subprocess as _sp
|
||||||
r = _sp.run(
|
r = _sp.run(
|
||||||
['ffmpeg', '-y', '-ss', str(t), '-i', video_path,
|
['ffmpeg', '-y', '-ss', str(t), '-i', video_path,
|
||||||
@@ -3390,7 +3495,36 @@ def _extract_frame_at(video_path: str, t: float) -> Image.Image:
|
|||||||
)
|
)
|
||||||
if r.returncode != 0 or not r.stdout:
|
if r.returncode != 0 or not r.stdout:
|
||||||
raise ValueError(f"ffmpeg frame extract failed: {r.stderr.decode(errors='replace')[:300]}")
|
raise ValueError(f"ffmpeg frame extract failed: {r.stderr.decode(errors='replace')[:300]}")
|
||||||
return Image.open(io.BytesIO(r.stdout)).convert("RGB")
|
img = Image.open(io.BytesIO(r.stdout)).convert("RGB")
|
||||||
|
|
||||||
|
# Save to wireframe directory as videoname-f{frame_number}.png
|
||||||
|
try:
|
||||||
|
import cv2
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if cap.isOpened():
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
cap.release()
|
||||||
|
frame_num = int(round(t * fps)) if fps > 0 else 0
|
||||||
|
frame_num = max(0, min(total - 1, frame_num))
|
||||||
|
else:
|
||||||
|
frame_num = int(round(t * 30.0))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error computing frame number: {e}")
|
||||||
|
frame_num = int(round(t * 30.0))
|
||||||
|
|
||||||
|
try:
|
||||||
|
wireframe_dir = os.path.dirname(video_path)
|
||||||
|
video_name = os.path.basename(video_path)
|
||||||
|
stem = os.path.splitext(video_name)[0]
|
||||||
|
out_name = f"{stem}-f{frame_num}.png"
|
||||||
|
out_path = os.path.join(wireframe_dir, out_name)
|
||||||
|
img.save(out_path, format="PNG")
|
||||||
|
print(f"[scenery] Saved extracted frame to {out_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving extracted frame: {e}")
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
class SceneryRequest(BaseModel):
|
class SceneryRequest(BaseModel):
|
||||||
@@ -3398,6 +3532,7 @@ class SceneryRequest(BaseModel):
|
|||||||
scene_bytes: str | None = None # base64-encoded PNG/JPEG of the reference scene → image1
|
scene_bytes: str | None = None # base64-encoded PNG/JPEG of the reference scene → image1
|
||||||
scene_video: str | None = None # wireframe video name to extract frame from
|
scene_video: str | None = None # wireframe video name to extract frame from
|
||||||
scene_time: float = 0.0 # timestamp (seconds) to extract from video
|
scene_time: float = 0.0 # timestamp (seconds) to extract from video
|
||||||
|
scene_image: str | None = None # optional wireframe image name to use directly
|
||||||
extra_filename: str | None = None # optional extra reference in output_dir → image3 (Picture 3)
|
extra_filename: str | None = None # optional extra reference in output_dir → image3 (Picture 3)
|
||||||
prompt: str | None = None # override; auto-built if None
|
prompt: str | None = None # override; auto-built if None
|
||||||
seed: int = -1
|
seed: int = -1
|
||||||
@@ -3420,7 +3555,8 @@ def _make_side_by_side(img1: Image.Image, img2: Image.Image,
|
|||||||
|
|
||||||
def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||||
prompt: str, seed: int, extra_pils: list | None = None,
|
prompt: str, seed: int, extra_pils: list | None = None,
|
||||||
scene_video: str | None = None, extra_filename: str | None = None):
|
scene_video: str | None = None, scene_image: str | None = None,
|
||||||
|
extra_filename: str | None = None):
|
||||||
output_dir = _load_output_dir()
|
output_dir = _load_output_dir()
|
||||||
try:
|
try:
|
||||||
model_path = os.path.join(output_dir, model_filename)
|
model_path = os.path.join(output_dir, model_filename)
|
||||||
@@ -3457,6 +3593,8 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
|||||||
refs = [model_filename]
|
refs = [model_filename]
|
||||||
if scene_video:
|
if scene_video:
|
||||||
refs.append(f"video:{scene_video}")
|
refs.append(f"video:{scene_video}")
|
||||||
|
if scene_image:
|
||||||
|
refs.append(f"wireframe:{scene_image}")
|
||||||
if extra_filename:
|
if extra_filename:
|
||||||
refs.append(extra_filename)
|
refs.append(extra_filename)
|
||||||
database.upsert_person(out_name, filepath=out_path, embedding=embedding,
|
database.upsert_person(out_name, filepath=out_path, embedding=embedding,
|
||||||
@@ -3491,6 +3629,11 @@ def generate_scenery(req: SceneryRequest):
|
|||||||
import base64
|
import base64
|
||||||
raw = base64.b64decode(req.scene_bytes)
|
raw = base64.b64decode(req.scene_bytes)
|
||||||
scene_pil = Image.open(io.BytesIO(raw)).convert("RGB")
|
scene_pil = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||||
|
elif req.scene_image:
|
||||||
|
image_path = os.path.join(wireframe_dir, req.scene_image)
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
raise HTTPException(404, f"Scene image not found: {req.scene_image}")
|
||||||
|
scene_pil = Image.open(image_path).convert("RGB")
|
||||||
elif req.scene_video:
|
elif req.scene_video:
|
||||||
video_path = os.path.join(wireframe_dir, req.scene_video)
|
video_path = os.path.join(wireframe_dir, req.scene_video)
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
@@ -3500,7 +3643,7 @@ def generate_scenery(req: SceneryRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"Frame extraction failed: {e}")
|
raise HTTPException(500, f"Frame extraction failed: {e}")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(400, "Provide scene_bytes or scene_video")
|
raise HTTPException(400, "Provide scene_bytes, scene_image, or scene_video")
|
||||||
|
|
||||||
# Optional image3 (Picture 3) — extra reference from output_dir
|
# Optional image3 (Picture 3) — extra reference from output_dir
|
||||||
extra_pils = []
|
extra_pils = []
|
||||||
@@ -3525,6 +3668,7 @@ def generate_scenery(req: SceneryRequest):
|
|||||||
target=_scenery_worker,
|
target=_scenery_worker,
|
||||||
args=(job_id, req.model_filename, scene_pil, prompt, req.seed, extra_pils),
|
args=(job_id, req.model_filename, scene_pil, prompt, req.seed, extra_pils),
|
||||||
kwargs={"scene_video": req.scene_video if req.scene_video else None,
|
kwargs={"scene_video": req.scene_video if req.scene_video else None,
|
||||||
|
"scene_image": req.scene_image if req.scene_image else None,
|
||||||
"extra_filename": req.extra_filename},
|
"extra_filename": req.extra_filename},
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
@@ -3558,6 +3702,15 @@ def scenery_library():
|
|||||||
except Exception:
|
except Exception:
|
||||||
refs = []
|
refs = []
|
||||||
video = next((r[len("video:"):] for r in refs if r.startswith("video:")), None)
|
video = next((r[len("video:"):] for r in refs if r.startswith("video:")), None)
|
||||||
|
if not video:
|
||||||
|
wf_img = next((r[len("wireframe:"):] for r in refs if r.startswith("wireframe:")), None)
|
||||||
|
if wf_img:
|
||||||
|
m = re.match(r"^(.*)-f\d+\.(png|jpg|jpeg|webp)$", wf_img, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
video = m.group(1)
|
||||||
|
else:
|
||||||
|
video = wf_img
|
||||||
|
|
||||||
entry = {"filename": filename, "group_id": group_id, "refs": refs}
|
entry = {"filename": filename, "group_id": group_id, "refs": refs}
|
||||||
if video:
|
if video:
|
||||||
by_video.setdefault(video, []).append(entry)
|
by_video.setdefault(video, []).append(entry)
|
||||||
|
|||||||
Reference in New Issue
Block a user