Files
qwen-image/tour-comfy/car.html
2026-06-28 04:51:24 +02:00

12659 lines
716 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Studio Monitor</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #e0e0e0;
min-height: 100vh;
}
.header {
position: fixed;
top: 0; left: 0; right: 0;
background: rgba(15, 15, 15, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid #333;
padding: 16px 24px;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 18px;
font-weight: 600;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
}
.status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #888;
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #22c55e;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-dot.updating {
background: #f59e0b;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.controls {
display: flex;
gap: 12px;
align-items: center;
}
.btn {
background: #1a1a1a;
border: 1px solid #333;
color: #ccc;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn:hover {
background: #252525;
border-color: #444;
color: #fff;
}
.btn.primary {
background: #2563eb;
border-color: #2563eb;
color: #fff;
}
.btn.primary:hover {
background: #3b82f6;
}
.btn.danger {
background: #991b1b;
border-color: #991b1b;
color: #fff;
}
.btn.danger:hover {
background: #b91c1c;
}
.image-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
z-index: 20;
}
.selectable .image-actions {
display: none !important;
}
.image-card:hover .image-actions {
opacity: 1;
}
.image-card.highlight-prev {
border: 2px solid #f59e0b !important;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.4);
}
.archive-confirm-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.85);
backdrop-filter: blur(4px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 50;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
padding: 12px;
text-align: center;
}
.image-card.confirming-archive .archive-confirm-overlay {
opacity: 1;
pointer-events: auto;
}
.archive-confirm-overlay h3 {
font-size: 14px;
margin: 0 0 12px 0;
color: #eee;
}
.archive-confirm-buttons {
display: flex;
gap: 8px;
}
.pad-preview-overlay {
position: absolute;
background: rgba(245,158,11,0.4);
border: 1px dashed #f59e0b;
pointer-events: none;
z-index: 10;
}
.action-btn {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,0.1);
color: #fff;
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: rgba(0,0,0,0.8);
border-color: rgba(255,255,255,0.3);
}
.action-btn.delete:hover {
background: #991b1b;
border-color: #991b1b;
}
.prompt-bar {
display: flex;
gap: 8px;
align-items: center;
}
.prompt-input {
background: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
width: 320px;
outline: none;
transition: border-color 0.2s;
}
.prompt-input:focus {
border-color: #2563eb;
}
.prompt-input::placeholder {
color: #555;
}
.count {
font-size: 13px;
color: #666;
font-variant-numeric: tabular-nums;
}
.gallery {
padding: 100px 24px 40px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
max-width: 1800px;
margin: 0 auto;
}
.image-card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s, border-color 0.2s;
position: relative;
}
.image-card:hover {
transform: translateY(-2px);
border-color: #444;
}
.image-card.new {
border-color: #2563eb;
animation: highlight 1s ease;
}
@keyframes highlight {
from { box-shadow: 0 0 0 2px #2563eb; }
to { box-shadow: none; }
}
.image-wrapper {
position: relative;
padding-top: 133%; /* 4:3 portrait */
background: #111;
overflow: hidden;
}
.image-wrapper img {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: contain;
transition: opacity 0.3s;
}
.variant-count {
position: absolute;
bottom: 8px; left: 8px;
background: rgba(0,0,0,0.65);
color: #fff;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 10px;
z-index: 5;
pointer-events: none;
}
.variant-dots {
position: absolute;
bottom: 8px; left: 50%; transform: translateX(-50%);
display: flex; gap: 4px;
z-index: 5;
}
.dot {
width: 6px; height: 6px;
border-radius: 50%;
background: rgba(255,255,255,0.35);
transition: background 0.2s;
}
.dot.active { background: #fff; }
.image-info {
padding: 14px 16px;
}
.image-name {
font-size: 14px;
font-weight: 500;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 6px;
}
.image-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
}
.image-time {
display: flex;
align-items: center;
gap: 4px;
}
.badge {
background: #2563eb;
color: #fff;
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge.recent {
background: #22c55e;
}
.empty-state {
text-align: center;
padding: 120px 20px;
color: #555;
}
.empty-state svg {
width: 64px; height: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.empty-state h2 {
font-size: 20px;
color: #777;
margin-bottom: 8px;
}
.empty-state p {
font-size: 14px;
}
.toast {
position: fixed;
bottom: 24px; right: 24px;
background: #1a1a1a;
border: 1px solid #333;
padding: 12px 20px;
border-radius: 8px;
font-size: 13px;
color: #ccc;
transform: translateY(100px);
opacity: 0;
transition: all 0.3s;
z-index: 200;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
.toast.success { border-left: 3px solid #22c55e; }
.toast.info { border-left: 3px solid #2563eb; }
.btn.active {
background: #2563eb;
border-color: #2563eb;
color: #fff;
}
.image-card.selectable {
cursor: pointer;
user-select: none;
}
.image-card.selected {
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37,99,235,0.35);
}
.select-indicator {
position: absolute;
top: 8px; left: 8px;
width: 22px; height: 22px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.5);
background: rgba(0,0,0,0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 15;
font-size: 13px;
font-weight: 700;
color: #fff;
transition: background 0.15s, border-color 0.15s;
pointer-events: none;
}
.selectable .select-indicator { display: flex; }
.selected .select-indicator {
background: #2563eb;
border-color: #2563eb;
}
.selected .select-indicator::after { content: '✓'; }
.variant-picker {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.80);
z-index: 350;
align-items: center;
justify-content: center;
}
.variant-picker.open { display: flex; }
.variant-picker-inner {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 14px;
padding: 22px;
max-width: 88vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 40px rgba(0,0,0,0.7);
}
.variant-picker-title {
color: #ccc;
font-size: 14px;
font-weight: 600;
margin-bottom: 14px;
}
.variant-picker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
}
.variant-thumb {
cursor: pointer;
border-radius: 8px;
overflow: hidden;
border: 2px solid transparent;
aspect-ratio: 3/4;
background: #111;
transition: border-color 0.15s, transform 0.15s;
}
.variant-thumb:hover { border-color: #2563eb; transform: scale(1.03); }
.variant-thumb.selected { border-color: #22c55e; }
.variant-thumb.hidden-img { opacity: 0.38; }
.variant-thumb.hidden-img::after {
content: '🚫';
position: absolute;
top: 4px; right: 4px;
font-size: 14px;
line-height: 1;
}
.variant-thumb { position: relative; }
.lb-hidden-badge {
position: absolute;
top: 12px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7);
color: #f87171;
font-size: 12px;
font-weight: 600;
padding: 3px 12px;
border-radius: 20px;
pointer-events: none;
display: none;
}
.lightbox.img-hidden .lb-hidden-badge { display: block; }
.lightbox.img-hidden #lbImg { opacity: 0.45; }
.studio.st-hidden .lb-hidden-badge { display: block; }
.studio.st-hidden #lbImg { opacity: 0.45; }
.variant-thumb img {
width: 100%; height: 100%;
object-fit: contain;
display: block;
}
.variant-picker-actions {
display: flex;
gap: 8px;
margin-top: 16px;
justify-content: flex-end;
}
.batch-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
background: rgba(12,12,12,0.97);
backdrop-filter: blur(12px);
border-top: 1px solid #2a2a2a;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
z-index: 100;
transform: translateY(100%);
transition: transform 0.25s ease;
}
.batch-bar.visible { transform: translateY(0); }
.pose-menu {
position: absolute;
bottom: 100%;
left: 24px;
right: 24px;
background: #1a1a1a;
border: 1px solid #333;
border-bottom: none;
border-radius: 8px 8px 0 0;
padding: 16px;
display: none;
flex-wrap: wrap;
gap: 8px;
max-height: 300px;
overflow-y: auto;
box-shadow: 0 -10px 20px rgba(0,0,0,0.5);
}
.pose-menu.open {
display: flex;
}
.pose-item {
background: #2a2a2a;
border: 1px solid #444;
color: #ccc;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.pose-item:hover {
background: #3a3a3a;
color: #fff;
}
.pose-item.active {
background: #2563eb;
border-color: #2563eb;
color: #fff;
}
.batch-count {
font-size: 13px;
color: #888;
min-width: 80px;
white-space: nowrap;
}
.batch-prompt-wrap { flex: 1; position: relative; }
.batch-prompt {
width: 100%;
background: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
padding: 7px 12px;
border-radius: 6px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.batch-prompt:focus { border-color: #2563eb; }
.batch-prompt::placeholder { color: #555; }
.batch-progress {
font-size: 12px;
color: #666;
min-width: 90px;
text-align: right;
white-space: nowrap;
}
/* Lightbox */
.lightbox {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.93);
z-index: 500;
align-items: center;
justify-content: center;
gap: 16px;
}
.lightbox.open { display: flex; }
.lightbox img {
max-width: 88vw;
max-height: 88vh;
object-fit: contain;
border-radius: 4px;
display: block;
}
.lb-btn {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
color: #fff;
border-radius: 8px;
cursor: pointer;
font-size: 20px;
padding: 10px 16px;
transition: background 0.2s;
flex-shrink: 0;
}
.lb-btn:hover { background: rgba(255,255,255,0.18); }
.lb-close {
position: absolute;
top: 16px; right: 20px;
font-size: 16px;
padding: 6px 12px;
}
.lb-info {
position: absolute;
bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; gap: 16px; align-items: center;
background: rgba(0,0,0,0.8);
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
color: white;
white-space: nowrap;
}
#lbGroupNameInput, #lbNameInput {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
color: white;
padding: 5px 10px;
border-radius: 4px;
width: 150px;
font-size: 13px;
}
#lbGroupNameInput { border-color: rgba(255,255,255,0.35); }
#lbClipDesc {
font-size: 12px;
color: #aaa;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.upload-zone {
display: flex;
align-items: center;
gap: 10px;
margin-left: 20px;
padding-left: 20px;
border-left: 1px solid #333;
}
#uploadInput { display: none; }
/* --- lightbox variant strip --- */
#lbVariantStrip {
display: none;
flex-direction: row;
gap: 6px;
overflow-x: auto;
padding: 6px 2px 2px;
width: 100%;
scrollbar-width: thin;
scrollbar-color: #444 transparent;
}
.lb-var-thumb {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s;
max-width: 60px;
}
.lb-var-thumb:hover { opacity: 0.85; }
.lb-var-thumb.active { opacity: 1; outline: 2px solid #60a5fa; border-radius: 4px; }
.lb-var-thumb.ref-selected { opacity: 1; outline: 2px solid #f97316; border-radius: 4px; }
.lb-var-thumb.active.ref-selected { outline: 2px solid #f97316; }
.lb-var-thumb.selected {
opacity: 1 !important;
outline: 2px solid #3b82f6 !important;
box-shadow: 0 0 8px rgba(59,130,246,0.6);
border-radius: 4px;
}
.lb-var-thumb-check {
position: absolute;
top: 2px;
right: 2px;
background: #3b82f6;
color: white;
border-radius: 50%;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: bold;
z-index: 5;
pointer-events: none;
}
.lb-var-ref-badge { position: absolute; top: 2px; left: 2px; background: #f97316; color: #fff; font-size: 9px; font-weight: bold; padding: 1px 4px; border-radius: 3px; line-height: 1.4; pointer-events: none; z-index: 2; }
.lb-var-thumb img {
width: 52px; height: 52px;
object-fit: cover;
border-radius: 4px;
display: block;
}
.lb-var-pose {
font-size: 7px;
color: #999;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 58px;
}
.lb-var-thumb { position: relative; }
.lb-var-play {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 13px;
color: #fff;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
pointer-events: none;
}
/* --- group card grid overview --- */
.card-grid {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px;
background: #111;
}
.card-grid-thumb {
position: relative;
overflow: hidden;
cursor: pointer;
}
.card-grid-thumb img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
}
.card-grid-thumb .thumb-pose {
position: absolute;
bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,0.72);
font-size: 7px;
color: #ccc;
padding: 1px 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
pointer-events: none;
}
.card-grid-more {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.55);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: #fff;
pointer-events: none;
}
/* --- privacy overlay: disguised as an innocuous AI study assistant --- */
.privacy-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 999999;
background: #0a0a0a;
color: #e0e0e0;
flex-direction: column;
cursor: default;
user-select: none;
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.priv-topbar {
display: flex; align-items: center; gap: 10px;
padding: 12px 18px;
border-bottom: 1px solid #333;
background: #1a1a1a;
font-size: 14px; font-weight: 600; color: #e0e0e0;
}
.priv-topbar .priv-logo {
width: 26px; height: 26px; border-radius: 6px;
background: linear-gradient(135deg,#2563eb,#1d4ed8);
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 14px; font-weight: 700;
}
.priv-topbar .priv-sub { font-weight: 400; color: #8a8a8a; font-size: 12px; margin-left: 2px; }
.priv-chat {
flex: 1; overflow-y: auto;
padding: 26px 0;
display: flex; flex-direction: column; align-items: center;
}
.priv-msg-row { width: 100%; padding: 18px 0; }
.priv-msg-row.assistant { background: #1a1a1a; border-top: 1px solid #333; border-bottom: 1px solid #333; }
.priv-msg-inner {
max-width: 720px; margin: 0 auto; padding: 0 24px;
display: flex; gap: 16px; align-items: flex-start;
}
.priv-avatar {
flex: 0 0 28px; width: 28px; height: 28px; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700; color: #fff;
}
.priv-avatar.user { background: #8b5cf6; }
.priv-avatar.bot { background: #2563eb; }
.priv-text { font-size: 15px; line-height: 1.7; color: #e0e0e0; }
.priv-text .eq { font-family: "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace; background: #2d333a; padding: 1px 5px; border-radius: 4px; }
.priv-inputbar {
border-top: 1px solid #333; background: #1a1a1a;
padding: 14px 0 22px;
}
.priv-inputbox {
max-width: 720px; margin: 0 auto; padding: 0 24px;
display: flex; align-items: center; gap: 10px;
}
.priv-fakeinput {
flex: 1; border: 1px solid #333; border-radius: 12px;
padding: 12px 16px; font-size: 14px; color: #888;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
background: #151515;
}
.priv-send { width: 34px; height: 34px; border-radius: 8px; background: #10a37f; color:#fff; display:flex; align-items:center; justify-content:center; font-size:15px; }
.priv-hint { text-align:center; font-size: 11px; color: #b0b0b0; margin-top: 10px; }
/* --- preferred star badge in variant picker --- */
.variant-thumb.preferred::before {
content: '★';
position: absolute;
top: 3px; left: 5px;
font-size: 14px;
color: #f59e0b;
text-shadow: 0 0 4px #000;
pointer-events: none;
z-index: 2;
}
.set-preferred-btn {
position: absolute;
bottom: 3px; right: 3px;
background: rgba(0,0,0,0.7);
color: #999;
border: none;
border-radius: 3px;
font-size: 10px;
padding: 1px 4px;
cursor: pointer;
z-index: 2;
}
.set-preferred-btn:hover { color: #f59e0b; }
/* ---- professional UI polish ---- */
body { background: #0a0a0a; }
.header {
background: rgba(10,10,10,0.96);
border-bottom: 1px solid #1e1e1e;
padding: 14px 20px;
gap: 12px;
}
.header h1 { font-size: 16px; letter-spacing: -0.2px; }
.header h1 span { color: #555; font-weight: 400; font-size: 13px; margin-left: 4px; }
.btn {
background: #181818;
border: 1px solid #2a2a2a;
color: #aaa;
padding: 5px 12px;
border-radius: 7px;
font-size: 12px;
height: 30px;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn:hover { background: #222; border-color: #3a3a3a; color: #eee; }
.btn.primary { background: #1d4ed8; border-color: #2563eb; color: #fff; }
.btn.primary:hover { background: #2563eb; }
.prompt-input { background: #111; border-color: #2a2a2a; border-radius: 7px; }
.prompt-input:focus { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.2); }
.image-card {
background: #111;
border: 1px solid #1a1a1a;
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.image-card:hover {
border-color: #2e2e2e;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
transform: translateY(-3px);
}
.image-info { padding: 12px 14px; }
.image-name { font-size: 13px; }
.image-meta { font-size: 11px; }
.gallery { padding-top: 96px; gap: 16px; }
.batch-bar {
background: rgba(8,8,8,0.98);
border-top: 1px solid #1e1e1e;
}
.batch-prompt { background: #111; border-color: #2a2a2a; border-radius: 7px; }
/* ---- video card support ---- */
.image-wrapper video {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
display: block;
border-radius: 0;
}
.video-play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
z-index: 4;
background: rgba(0,0,0,0.25);
}
.image-card:hover .video-play-overlay { opacity: 1; }
.video-play-icon {
width: 42px; height: 42px;
background: rgba(255,255,255,0.88);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
}
.badge.video { background: #6d28d9; }
.badge.fs { background: #0f766e; }
/* ---- faceswap modal ---- */
.faceswap-modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.88);
z-index: 600;
align-items: center;
justify-content: center;
}
.faceswap-modal.open { display: flex; }
.faceswap-modal-inner {
background: #111;
border: 1px solid #252525;
border-radius: 16px;
padding: 22px 28px 24px;
max-width: 640px;
width: 92vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 24px 72px rgba(0,0,0,0.9);
}
/* ---- modal header ---- */
.fs-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.fs-model-thumb {
width: 52px; height: 52px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #2a2a2a;
flex-shrink: 0;
}
.fs-header-info { min-width: 0; }
.faceswap-modal-title {
font-size: 15px;
font-weight: 650;
color: #fff;
margin-bottom: 2px;
}
.faceswap-modal-sub {
font-size: 11px;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ---- tabs ---- */
.fs-tabs {
display: flex;
gap: 0;
margin-bottom: 14px;
border-bottom: 1px solid #222;
}
.fs-tab {
background: none;
border: none;
color: #555;
font-size: 12px;
font-weight: 500;
padding: 7px 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
}
.fs-tab:hover { color: #aaa; }
.fs-tab.active { color: #e4e4e7; border-bottom-color: #7c3aed; }
.fs-tab-content { display: none; }
.fs-tab-content.active { display: block; }
/* ---- poses grid (generate tab) ---- */
.fs-poses-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 200px;
overflow-y: auto;
padding: 2px 0 10px;
}
.fs-pose-btn {
padding: 5px 11px;
border-radius: 6px;
border: 1px solid #2a2a2a;
background: #18181b;
color: #888;
font-size: 11px;
cursor: pointer;
user-select: none;
transition: all 0.13s;
}
.fs-pose-btn:hover { border-color: #444; color: #ccc; }
.fs-pose-btn.done {
border-color: #14532d;
background: #052e16;
color: #4ade80;
}
.fs-pose-btn.done::before { content: '✓ '; opacity: 0.8; }
.fs-pose-btn.selected {
border-color: #6d28d9;
background: #1e1040;
color: #c4b5fd;
font-weight: 600;
}
.fs-pose-btn.done.selected { border-color: #6d28d9; background: #1e1040; color: #c4b5fd; }
.fs-prompt-row { margin-top: 10px; }
.fs-prompt-input {
width: 100%; box-sizing: border-box;
background: #18181b;
border: 1px solid #2a2a2a;
border-radius: 7px;
color: #e4e4e7;
font-size: 12px;
padding: 8px 10px;
}
.fs-prompt-input:focus { outline: none; border-color: #7c3aed; }
.fs-prompt-input::placeholder { color: #3f3f46; }
/* ---- trim tool ---- */
.template-card .trim-btn {
position: absolute;
top: 5px; right: 5px;
width: 24px; height: 24px;
background: rgba(0,0,0,0.72);
border: 1px solid #333;
border-radius: 5px;
color: #888;
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.15s, color 0.1s;
z-index: 3;
}
.template-card:hover .trim-btn { opacity: 1; }
.template-card .trim-btn:hover { color: #fff; background: rgba(0,0,0,0.92); }
.fs-trim-panel {
background: #0d0d10;
border: 1px solid #222;
border-radius: 10px;
padding: 14px 16px;
margin-bottom: 12px;
}
.fs-trim-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 12px;
color: #aaa;
font-weight: 600;
}
.fs-trim-close {
background: none; border: none;
color: #555; cursor: pointer;
font-size: 15px; line-height: 1;
}
.fs-trim-close:hover { color: #ccc; }
.fs-trim-form {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: flex-end;
}
.fs-trim-form label {
display: flex; flex-direction: column; gap: 4px;
font-size: 11px; color: #555;
}
.fs-trim-form input[type=number],
.fs-trim-form input[type=text] {
background: #18181b;
border: 1px solid #2a2a2a;
border-radius: 5px;
color: #e4e4e7;
font-size: 12px;
padding: 5px 8px;
width: 84px;
}
.fs-trim-form input[type=text] { width: 150px; }
.fs-trim-form input:focus { outline: none; border-color: #7c3aed; }
.fs-trim-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
align-items: center;
margin-top: 10px;
}
.fs-trim-status { font-size: 11px; color: #666; margin-right: auto; }
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 22px;
}
.template-card {
background: #181818;
border: 2px solid #222;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
aspect-ratio: 16/9;
position: relative;
}
.template-card:hover {
border-color: #7c3aed;
transform: scale(1.03);
box-shadow: 0 4px 20px rgba(124,58,237,0.25);
}
.template-card.selected {
border-color: #22c55e;
box-shadow: 0 4px 20px rgba(34,197,94,0.2);
}
.template-card video {
width: 100%; height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.template-card-label {
position: absolute;
bottom: 0; left: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: #ddd;
font-size: 11px;
font-weight: 500;
padding: 12px 8px 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.template-empty {
color: #444;
font-size: 13px;
text-align: center;
padding: 30px;
grid-column: 1/-1;
}
.faceswap-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
}
.fs-job-status {
font-size: 12px;
color: #888;
margin-right: auto;
display: flex;
align-items: center;
gap: 8px;
}
.fs-spinner {
width: 12px; height: 12px;
border: 2px solid #333;
border-top-color: #7c3aed;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
will-change: transform;
}
.fs-options {
display: flex;
gap: 18px;
padding: 8px 4px 0;
align-items: center;
}
.fs-opt-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #aaa;
cursor: pointer;
user-select: none;
}
.fs-opt-label input[type=checkbox] {
accent-color: #7c3aed;
width: 14px; height: 14px;
cursor: pointer;
}
.fs-opt-label input[type=checkbox]:disabled { opacity: 0.4; cursor: not-allowed; }
.fs-opt-badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 8px;
background: #27272a;
color: #666;
border: 1px solid #333;
}
/* ---- lb-info tighter layout ---- */
.lb-info {
padding: 8px 16px;
border-radius: 14px;
gap: 10px;
flex-wrap: wrap;
}
.lb-section-sep {
width: 1px; height: 18px;
background: rgba(255,255,255,0.15);
flex-shrink: 0;
}
/* ===================== STUDIO VIEW ===================== */
.studio {
display: none;
position: fixed;
inset: 0;
background: #0a0a0a;
z-index: 500;
flex-direction: column;
}
.studio.open { display: flex; }
.studio-topbar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: #0f0f0f;
border-bottom: 1px solid #1a1a1a;
height: 44px;
}
.studio-topbar-left { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.studio-topbar-center{ flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; overflow: hidden; }
.studio-topbar-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.studio-body {
flex: 1; min-height: 0;
display: flex; overflow: hidden;
}
.studio-main {
flex: 1; min-width: 0;
display: flex; flex-direction: column; overflow: hidden;
}
.studio-viewer-row {
flex: 1; min-height: 0;
display: flex; align-items: center; gap: 6px; padding: 10px 6px;
}
.studio-viewer {
flex: 1; min-width: 0; min-height: 0;
display: flex; align-items: center; justify-content: center;
position: relative; height: 100%; border-radius: 6px; overflow: hidden;
}
.studio-viewer.show-checker {
background-color: transparent;
}
#studioCheckerboard {
background-color: #888;
background-image:
linear-gradient(45deg,#555 25%,transparent 25%),
linear-gradient(-45deg,#555 25%,transparent 25%),
linear-gradient(45deg,transparent 75%,#555 75%),
linear-gradient(-45deg,transparent 75%,#555 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
z-index: 1;
}
#lbImg {
max-width: 100%; max-height: 100%;
object-fit: contain; border-radius: 4px;
cursor: zoom-in; transition: transform 0.25s; display: block;
z-index: 2; position: relative;
}
#lbImg.zoomed { cursor: zoom-out; transform: scale(2.2); transform-origin: center; }
#lbVideo { max-width: 100%; max-height: 100%; border-radius: 4px; }
.studio-nav-btn {
flex-shrink: 0; background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1); color: #fff;
border-radius: 8px; cursor: pointer; font-size: 26px;
width: 38px; height: 56px;
display: flex; align-items: center; justify-content: center;
transition: background 0.2s; user-select: none;
}
.studio-nav-btn:hover { background: rgba(255,255,255,0.14); }
/* Film strip */
.studio-filmstrip {
flex-shrink: 0; height: 76px;
display: flex; flex-direction: row; gap: 5px;
overflow-x: auto; padding: 8px 10px;
background: #0d0d0d; border-top: 1px solid #1a1a1a;
scrollbar-width: thin; scrollbar-color: #333 transparent;
align-items: center;
}
.fs-tab {
background: #222;
border: 1px solid #333;
color: #888;
font-size: 11px;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.fs-tab.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
font-weight: bold;
}
.fs-tab:hover:not(.active) {
background: #2d2d2d;
border-color: #444;
color: #fff;
}
.fs-action-btn {
background: #1e3a8a;
border: 1px solid #3b82f6;
color: #fff;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
}
.fs-action-btn:hover {
background: #3b82f6;
}
/* ===================== SIDEBAR ===================== */
.studio-sidebar {
flex-shrink: 0; width: 320px; min-width: 320px;
display: flex; flex-direction: column;
background: #0d0d10; border-left: 1px solid #1a1a1a;
transition: width 0.2s ease, min-width 0.2s ease;
overflow: hidden; position: relative;
}
.studio-sidebar.collapsed { width: 32px; min-width: 32px; }
.sidebar-toggle-btn {
position: absolute; top: 9px; left: 5px;
width: 22px; height: 22px;
background: #1a1a1a; border: 1px solid #2a2a2a;
border-radius: 4px; color: #666; font-size: 11px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
z-index: 2; transition: color 0.15s;
}
.sidebar-toggle-btn:hover { color: #fff; border-color: #444; }
.sidebar-inner {
flex: 1; min-height: 0;
display: flex; flex-direction: column;
padding-top: 36px; overflow: hidden;
transition: opacity 0.15s;
}
.studio-sidebar.collapsed .sidebar-inner { opacity: 0; pointer-events: none; }
.sidebar-tabs {
display: flex; border-bottom: 1px solid #1a1a1a;
overflow-x: auto; flex-shrink: 0; scrollbar-width: none;
}
.sidebar-tabs::-webkit-scrollbar { display: none; }
.sb-tab {
background: none; border: none; color: #555;
font-size: 11px; font-weight: 500; padding: 6px 10px;
cursor: pointer; border-bottom: 2px solid transparent;
margin-bottom: -1px; white-space: nowrap;
transition: color 0.15s, border-color 0.15s;
}
.sb-tab:hover { color: #999; }
.sb-tab.active { color: #e4e4e7; border-bottom-color: #2563eb; }
.sb-panels { flex: 1; min-height: 0; overflow: hidden; position: relative; }
.sb-panel {
display: none; flex-direction: column;
height: 100%; overflow-y: auto;
padding: 12px 13px;
scrollbar-width: thin; scrollbar-color: #2a2a2a transparent;
}
.sb-panel.active { display: flex; }
.sb-label {
font-size: 10px; color: #4a4a4a; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.6px;
margin-bottom: 5px;
}
.sb-input {
width: 100%; box-sizing: border-box;
background: #18181b; border: 1px solid #2a2a2a;
border-radius: 6px; color: #e4e4e7;
font-size: 12px; padding: 7px 10px; outline: none;
transition: border-color 0.15s;
}
.sb-input:focus { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.15); }
.sb-input::placeholder { color: #3f3f46; }
.sb-sep { height: 1px; background: #1a1a1a; margin: 10px 0; flex-shrink: 0; }
.sb-actions { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 4px; }
.sb-btn {
background: #18181b; border: 1px solid #2a2a2a;
color: #999; font-size: 11px; padding: 5px 9px;
border-radius: 6px; cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
display: inline-flex; align-items: center; gap: 4px;
text-decoration: none;
}
.sb-btn:hover { background: #252525; color: #eee; border-color: #3a3a3a; }
.sb-btn.primary { background: #1d4ed8; border-color: #2563eb; color: #fff; }
.sb-btn.primary:hover { background: #2563eb; }
.sb-btn.danger { background: #7f1d1d; border-color: #991b1b; color: #fca5a5; }
.sb-btn.danger:hover { background: #991b1b; }
.sb-btn.teal { background: #0f4c46; border-color: #0f766e; color: #99f6e4; }
.sb-btn.teal:hover { background: #0f766e; }
.sb-btn.purple { background: #3b0764; border-color: #6d28d9; color: #c4b5fd; }
.sb-btn.purple:hover { background: #6d28d9; }
.sb-btn.amber { background: #431407; border-color: #92400e; color: #fcd34d; }
.sb-btn.amber:hover { background: #78350f; }
.sb-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
/* Orbit preview modal */
#orbitModal {
display: none; position: fixed; inset: 0; z-index: 9000;
background: rgba(0,0,0,0.85); align-items: center; justify-content: center;
}
#orbitModal.open { display: flex; }
#orbitModal .orbit-box {
background: #111; border: 1px solid #2a2a2a; border-radius: 10px;
padding: 20px; max-width: 600px; width: 90%; position: relative;
}
#orbitModal .orbit-title {
font-size: 13px; font-weight: 600; color: #ccc; margin-bottom: 12px;
}
#orbitModal video {
width: 100%; border-radius: 6px; background: #000;
}
#orbitModal .orbit-status {
font-size: 11px; color: #666; margin: 8px 0 0;
}
#orbitModal .orbit-status.ok { color: #4ade80; }
#orbitModal .orbit-status.err { color: #f87171; }
#orbitModal .orbit-close {
position: absolute; top: 10px; right: 12px;
background: none; border: none; color: #555; font-size: 18px;
cursor: pointer; line-height: 1;
}
#orbitModal .orbit-close:hover { color: #ccc; }
#orbitModal .orbit-actions {
display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; align-items: center;
}
.orbit-engine-toggle { display: flex; gap: 4px; margin-bottom: 10px; }
.orbit-engine-toggle button {
flex: 1; padding: 5px 10px; font-size: 11px; border-radius: 4px;
border: 1px solid #333; background: #1a1a1a; color: #777; cursor: pointer;
}
.orbit-engine-toggle button.active {
background: #2563eb; color: #fff; border-color: #2563eb;
}
.orbit-qwen-controls { display: none; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 6px; }
.orbit-qwen-controls.visible { display: flex; }
#turntableBgStatus {
font-size: 10px; color: #555; margin-top: 4px;
display: flex; align-items: center; gap: 4px;
}
#turntableBgStatus .tbs-dot {
width: 6px; height: 6px; border-radius: 50%; background: #555; flex-shrink: 0;
}
#turntableBgStatus .tbs-dot.active { background: #f59e0b; animation: pulse 1.2s ease-in-out infinite; }
#turntableBgStatus .tbs-dot.done { background: #4ade80; animation: none; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
.orbit-badge {
font-size: 9px; background: #f59e0b; color: #000; border-radius: 9px;
padding: 1px 5px; margin-left: 4px; font-weight: 700; vertical-align: middle;
}
/* Frame flipper player */
.orbit-player { display: none; flex-direction: column; align-items: stretch; gap: 0; }
.orbit-player.visible { display: flex; }
/* Fixed-height image container — prevents layout jumps when frames differ in size */
.orbit-img-wrap {
width: 100%; aspect-ratio: 3/4; position: relative; border-radius: 6px; overflow: hidden;
background: repeating-conic-gradient(#222 0% 25%, #2a2a2a 0% 50%) 0 0 / 16px 16px;
flex-shrink: 0;
}
.orbit-img-wrap img {
position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: contain; display: block;
}
/* Controls bar is fixed height — never moves */
.orbit-player-controls {
display: flex; gap: 8px; align-items: center; flex-wrap: nowrap;
justify-content: space-between; width: 100%; padding: 6px 0 0;
flex-shrink: 0; min-height: 32px;
}
.orbit-player-controls button { padding: 3px 10px; font-size: 11px; flex-shrink: 0; }
.orbit-frame-counter { font-size: 10px; color: #555; min-width: 48px; text-align: center; flex-shrink: 0; }
/* Orbit sidebar tab */
#sbPanelOrbit { flex-direction: column; gap: 0; overflow-y: auto; }
.orbit-tab-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 6px 0; }
.orbit-tab-card {
background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 6px;
padding: 6px; cursor: pointer; transition: border-color .15s;
display: flex; flex-direction: column; gap: 4px; align-items: center;
}
.orbit-tab-card:hover { border-color: #f59e0b; }
/* Card thumbnails: fixed square with contain (shows whole body, no crop) */
.orbit-tab-card .otc-img-wrap {
width: 100%; aspect-ratio: 1/1; position: relative; border-radius: 4px; overflow: hidden;
background: repeating-conic-gradient(#222 0% 25%, #2a2a2a 0% 50%) 0 0 / 10px 10px;
flex-shrink: 0;
}
.orbit-tab-card .otc-img-wrap img {
position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
}
.orbit-tab-card .otc-name { font-size: 10px; color: #aaa; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; }
.orbit-tab-card .otc-progress { font-size: 9px; color: #555; flex-shrink: 0; }
.orbit-tab-card.complete { border-color: #1a3a1a; }
.orbit-tab-card.complete:hover { border-color: #4ade80; }
/* Full viewer in sidebar tab */
.orbit-full-player { display: none; flex-direction: column; gap: 6px; padding: 0 0 8px; flex-shrink: 0; }
.orbit-full-player.visible { display: flex; }
.orbit-full-img-wrap {
width: 100%; aspect-ratio: 3/4; position: relative; border-radius: 6px; overflow: hidden;
background: repeating-conic-gradient(#222 0% 25%, #2a2a2a 0% 50%) 0 0 / 16px 16px;
flex-shrink: 0;
}
.orbit-full-img-wrap img {
position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
}
.sb-status { font-size: 11px; color: #555; margin: 4px 0; }
.sb-status.ok { color: #4ade80; }
.sb-status.err { color: #f87171; }
.sb-spinner {
width: 10px; height: 10px; border: 2px solid #333;
border-top-color: #2563eb; border-radius: 50%;
animation: spin 0.8s linear infinite; display: inline-block;
margin-right: 4px; vertical-align: middle;
will-change: transform;
}
.sb-source-refs { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.sb-source-refs img {
width: 28px; height: 28px; object-fit: cover; border-radius: 3px;
cursor: pointer; opacity: 0.7; border: 1px solid #333;
transition: opacity 0.15s, border-color 0.15s;
}
.sb-source-refs img:hover { opacity: 1; border-color: #555; }
/* poses grid in Generate tab */
.sb-poses-grid { display: flex; flex-wrap: wrap; gap: 5px; padding-bottom: 8px; }
.sb-pose-btn {
padding: 4px 10px; border-radius: 5px;
border: 1px solid #2a2a2a; background: #18181b;
color: #777; font-size: 11px; cursor: pointer; user-select: none;
transition: all 0.12s;
}
.sb-pose-btn:hover { border-color: #444; color: #ccc; }
.sb-pose-btn.done { border-color: #14532d; background: #052e16; color: #4ade80; }
.sb-pose-btn.done::before { content: '✓ '; opacity: 0.8; }
.sb-pose-btn.selected { border-color: #6d28d9; background: #1e1040; color: #c4b5fd; font-weight: 600; }
.sb-pose-btn.beta-badge::after {
content: 'β'; font-size: 8px; background: #78350f; color: #fbbf24;
padding: 1px 3px; border-radius: 2px; margin-left: 3px; vertical-align: middle;
}
.sb-camera-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 4px; margin-bottom: 8px; }
.sb-angle-btn {
padding: 5px 2px; border-radius: 5px;
border: 1px solid #2a2a2a; background: #18181b;
color: #777; font-size: 11px; cursor: pointer; user-select: none;
text-align: center; transition: all 0.12s;
}
.sb-angle-btn:hover { border-color: #444; color: #ccc; }
.sb-angle-btn.done { border-color: #14532d; background: #052e16; color: #4ade80; position: relative; }
.sb-angle-btn.done::after { content: '✓'; position: absolute; top: 1px; right: 3px; font-size: 9px; color: #4ade80; opacity: 0.9; }
.sb-angle-btn.selected { border-color: #0e7490; background: #082f49; color: #7dd3fc; font-weight: 600; }
/* Generate panel: sticky footer (custom prompt + Generate) and selected-ref thumbnails */
.sb-gen-footer {
position: sticky; bottom: 0; margin: 8px -13px -12px; padding: 10px 13px;
background: #0d0d10; border-top: 1px solid #1f1f24;
}
.sb-gen-refs { display: flex; gap: 5px; margin-bottom: 8px; flex-wrap: wrap; }
.sb-gen-refs .sb-gen-ref { position: relative; width: 40px; height: 40px; flex-shrink: 0; }
.sb-gen-refs .sb-gen-ref img {
width: 40px; height: 40px; object-fit: cover; border-radius: 4px;
border: 1px solid #3d2008; background: #111;
}
.sb-gen-refs .sb-gen-ref-badge {
position: absolute; top: -4px; left: -4px; background: #f97316; color: #111;
font-size: 9px; font-weight: 700; border-radius: 50%; width: 15px; height: 15px;
display: flex; align-items: center; justify-content: center; line-height: 1;
}
.sb-gen-refs .sb-gen-ref-x {
position: absolute; top: -4px; right: -4px; background: #dc2626; color: #fff;
font-size: 11px; font-weight: 700; border-radius: 50%; width: 15px; height: 15px;
display: none; align-items: center; justify-content: center; line-height: 1;
}
.sb-gen-refs .sb-gen-ref:hover .sb-gen-ref-x { display: flex; }
.sb-gen-refs .sb-gen-ref:hover img { border-color: #dc2626; }
#studioAngleBar {
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s;
z-index: 10; pointer-events: none;
}
.studio-viewer:hover #studioAngleBar { opacity: 1; pointer-events: all; }
#studioAngleBar button {
font-size: 15px; background: rgba(0,0,0,0.65); border: 1px solid rgba(255,255,255,0.18);
border-radius: 4px; color: #fff; width: 34px; height: 34px; cursor: pointer; line-height: 1;
transition: background 0.12s; position: relative;
}
#studioAngleBar button:hover { background: rgba(255,255,255,0.18); }
#studioAngleBar button[title]:hover::after {
content: attr(title); position: absolute; bottom: 38px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.8); color: #fff; font-size: 10px; padding: 2px 6px;
border-radius: 3px; white-space: nowrap; pointer-events: none;
}
/* template grid (faceswap / scenery) */
.sb-template-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: 6px; margin-bottom: 8px;
}
.sb-template-card {
background: #181818; border: 2px solid #1e1e1e;
border-radius: 7px; overflow: hidden; cursor: pointer;
aspect-ratio: 16/9; position: relative;
transition: border-color 0.15s, transform 0.13s;
}
.sb-template-card:hover { border-color: #7c3aed; transform: scale(1.02); }
.sb-template-card.selected { border-color: #22c55e; }
.sb-template-card video,
.sb-template-card img { width:100%; height:100%; object-fit:cover; pointer-events:none; }
.sb-template-card video { position:absolute; inset:0; }
.sb-template-label {
position: absolute; bottom: 0; left: 0; right: 0;
background: linear-gradient(transparent,rgba(0,0,0,0.82));
color: #ccc; font-size: 9px; padding: 10px 5px 3px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none;
}
.sb-template-trim-btn {
position: absolute; top: 3px; right: 3px;
width: 20px; height: 20px;
background: rgba(0,0,0,0.7); border: 1px solid #333;
border-radius: 4px; color: #888; font-size: 11px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s, color 0.1s;
z-index: 2;
}
.sb-template-card:hover .sb-template-trim-btn { opacity: 1; }
.sb-template-trim-btn:hover { color: #fff; background: rgba(0,0,0,0.92); }
/* faceswap options */
.sb-opt-label {
display: flex; align-items: center; gap: 6px;
font-size: 11px; color: #888; cursor: pointer; user-select: none;
}
.sb-opt-label input[type=checkbox] { accent-color: #7c3aed; width: 13px; height: 13px; cursor: pointer; }
.sb-opt-label input[type=checkbox]:disabled { opacity: 0.4; cursor: not-allowed; }
.sb-opt-badge {
font-size: 9px; padding: 1px 5px; border-radius: 7px;
background: #27272a; color: #555; border: 1px solid #333;
}
/* ===================== CLIP TIMELINE ===================== */
.clt-wrapper {
background: #0d0d10; border: 1px solid #1e1e1e;
border-radius: 8px; overflow: hidden; margin: 8px 0;
}
.clt-video-preview {
width: 100%; aspect-ratio: 16/9; background: #000; display: block;
}
.clt-body { padding: 10px 12px 12px; }
.clt-track {
position: relative; height: 32px; background: #1a1a1a;
border-radius: 4px; cursor: pointer; user-select: none;
margin-bottom: 6px; overflow: visible;
}
.clt-bg { position: absolute; inset: 0; background: #222; border-radius: 4px; }
.clt-range {
position: absolute; top: 0; bottom: 0;
background: rgba(37,99,235,0.3); pointer-events: none;
}
.clt-handle {
position: absolute; top: 0; bottom: 0; width: 10px;
background: #2563eb; border-radius: 3px; cursor: ew-resize;
transform: translateX(-50%); z-index: 2;
display: flex; align-items: center; justify-content: center;
touch-action: none;
}
.clt-handle::after {
content: ''; width: 2px; height: 14px;
background: rgba(255,255,255,0.55); border-radius: 1px;
}
.clt-handle.end { background: #7c3aed; }
.clt-playhead {
position: absolute; top: -3px; bottom: -3px; width: 2px;
background: #f59e0b; pointer-events: none; z-index: 1;
transform: translateX(-50%);
}
.clt-labels {
display: flex; justify-content: space-between;
font-size: 10px; color: #555; margin-bottom: 8px;
}
.clt-name { font-size: 10px; color: #444; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ===================== SCENERY TAB ===================== */
.scene-frame-preview {
width: 100%; aspect-ratio: 16/9; background: #111;
border-radius: 6px; overflow: hidden; display: none;
margin-bottom: 8px; position: relative;
}
.scene-frame-preview img { width:100%; height:100%; object-fit:contain; display:block; }
.scene-scrubber { width: 100%; accent-color: #2563eb; margin: 6px 0 2px; cursor: pointer; height: 18px; }
.scene-scrubber:disabled { opacity: 0.4; cursor: not-allowed; }
.scene-captured-badge {
position: absolute; top: 6px; left: 6px;
background: rgba(34,197,94,0.9); color: #04210f;
border-radius: 5px; padding: 2px 8px; font-size: 11px; font-weight: 600;
}
/* 3 interactive reference slots (image1 / image2 / image3) */
.scene-slots { display: flex; gap: 6px; margin-bottom: 12px; }
.scene-slot {
flex: 1; min-width: 0; background: #141414; border: 1px solid #262626;
border-radius: 7px; overflow: hidden; text-align: center;
}
.scene-slot.filled { border-color: #2563eb; cursor: grab; }
.scene-slot.empty { border-style: dashed; }
.scene-slot.drop-hover { border-color: #60a5fa; background: #11203f; }
.scene-slot-thumb {
width: 100%; aspect-ratio: 1/1; background: #0c0c0c; position: relative;
display: flex; align-items: center; justify-content: center;
}
.scene-slot-thumb img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.scene-slot-thumb .ph { font-size: 18px; color: #333; }
.scene-slot-x {
position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; line-height: 14px;
background: rgba(0,0,0,0.7); border: 1px solid #444; border-radius: 4px;
color: #ddd; font-size: 10px; cursor: pointer; padding: 0;
}
.scene-slot-x:hover { background: #b91c1c; color: #fff; }
.scene-slot-kind { position: absolute; bottom: 1px; left: 2px; font-size: 10px; }
.scene-slot-cap { font-size: 8.5px; color: #888; padding: 3px 2px 2px; line-height: 1.25; }
.scene-slot-cap b { color: #bbb; display: block; font-size: 9px; }
/* ===================== SEGMENT TAB ===================== */
.segment-checker-hint { font-size: 10px; color: #444; margin-top: 6px; }
</style>
</head>
<body>
<div id="privacyOverlay" class="privacy-overlay" onclick="if(privacyMode)disablePrivacyOverlay()">
<div class="priv-topbar">
<div class="priv-logo">S</div>
AI Assist <span class="priv-sub">· Qwen-Image-Edit Rapid-AIO v23</span>
</div>
<div class="priv-chat">
<div class="priv-msg-row">
<div class="priv-msg-inner">
<div class="priv-avatar user">You</div>
<div class="priv-text">What is this system and what does it run on?</div>
</div>
</div>
<div class="priv-msg-row assistant">
<div class="priv-msg-inner">
<div class="priv-avatar bot">AI</div>
<div class="priv-text">
This is a self-hosted AI for generative editing, multi-view character rendering, background removal, face swapping, and group-based asset management. It runs on a local RTX A6000 GPU (CUDA 12.4, 48 GB VRAM) using the <strong>Qwen 2.5 VL GGUF</strong> model (Rapid-NSFW-v23 Q8_0) via ComfyUI. The frontend is a single-page browser app (car.html) talking to a FastAPI backend on :8500.
</div>
</div>
</div>
<div class="priv-msg-row">
<div class="priv-msg-inner">
<div class="priv-avatar user">You</div>
<div class="priv-text">Walk me through the image generation flow.</div>
</div>
</div>
<div class="priv-msg-row assistant">
<div class="priv-msg-inner">
<div class="priv-avatar bot">AI</div>
<div class="priv-text">
When a generation is triggered the pipeline is:<br><br>
<strong>1. Prompt cleaning</strong> — if the prompt contains "transparent background", that keyword is stripped so Qwen renders a clean solid-BG image instead of producing washed-out halos.<br><br>
<strong>2. ComfyUI queue</strong> — the reference image is uploaded to ComfyUI (:8188) and injected into <em>image1</em>. An optional wireframe frame extracted from a pose-guide video goes into <em>image2</em>. The workflow graph (workflow_qwen_edit.json) is patched with the prompt, dimensions, seed, and sampler params, then submitted via POST /prompt.<br><br>
<strong>3. Qwen inference</strong> — the GGUF model (≈8 GB VRAM), text encoder (FP8), and VAE decode the latent into a PNG.<br><br>
<strong>4. SAM2 post-processing</strong> — if transparent was requested, SAM2 point-prompt segmentation removes the background using 12 foreground seed points (hair → shoes, shoulder / hip lateral spread) and 7 background points (corners + edge midpoints). A coverage sanity check (592 %) falls back to rembg if the mask is implausible.
</div>
</div>
</div>
<div class="priv-msg-row">
<div class="priv-msg-inner">
<div class="priv-avatar user">You</div>
<div class="priv-text">What are the main API endpoints?</div>
</div>
</div>
<div class="priv-msg-row assistant">
<div class="priv-msg-inner">
<div class="priv-avatar bot">AI</div>
<div class="priv-text">
The FastAPI service (:8500) exposes:<br><br>
<strong>GET /images</strong> — full image list with PostgreSQL metadata (group, prompt, pose, sort order, archived flag).<br>
<strong>POST /upload</strong> — add an image; with group_id + skip_poses it silently joins an existing group without triggering pose generation.<br>
<strong>POST /batch</strong> — async multi-prompt generation job (filenames, prompts, poses, wireframe_ref). Poll progress via GET /jobs/{id}.<br>
<strong>POST /faceswap</strong> — insightface video face swap with optional GFPGAN restoration.<br>
<strong>POST /remove-background-sam/{f}</strong> — SAM2 BG removal to a .nobg.png sidecar.<br>
<strong>POST /images/{f}/set-preferred</strong> — moves image to group slot 0 and queues face extraction in the background.<br>
<strong>GET /wireframe/frame/{name}?t=</strong> — extract a frame at normalised time t∈[0,1] from a wireframe pose video.
</div>
</div>
</div>
<div class="priv-msg-row">
<div class="priv-msg-inner">
<div class="priv-avatar user">You</div>
<div class="priv-text">How does the face-book and multi-view workflow work?</div>
</div>
</div>
<div class="priv-msg-row assistant">
<div class="priv-msg-inner">
<div class="priv-avatar bot">AI</div>
<div class="priv-text">
<strong>Face-book:</strong> marking an image as "preferred" (star button) moves it to sort_order=0 in the group and triggers a background task. insightface detects the largest face, adds 50 % padding on the sides and 200 % headroom above, and saves a <em>{group_id}_face.png</em> crop. This appears as a 72 px thumbnail in the studio Info tab for fast face-reference lookup.<br><br>
<strong>Multi-view / camera angles:</strong> the Generate tab offers 12 camera angle presets — 8 absolute (Front, ¾ Left/Right, Side, Back, High, Low) and 4 relative (±45°, ±90°). Each fires a /batch request with the angle prompt injected and pose=null. The wireframe pose guide (a .mp4 video scrubbed to any frame) is passed as image2 in the Qwen workflow to constrain body layout without ControlNet — the controlnet model folder is empty by design.<br><br>
<strong>Group management:</strong> all images belong to groups stored in PostgreSQL. Clipboard paste (Ctrl+V) while a group is open offers "add to this group — no poses" or "new group with pose generation". Archive hides images from the default view without deleting them.
</div>
</div>
</div>
</div>
<div class="priv-inputbar">
<div class="priv-inputbox">
<div class="priv-fakeinput">Ask a follow-up question…</div>
<div class="priv-send"></div>
</div>
<div class="priv-hint">Press P or click to resume • Qwen-Image-Edit Rapid-AIO v23</div>
</div>
<!-- Inject build timestamp and math art -->
<script>
// Add build timestamp to privacy screen
function injectBuildTime() {
try {
const now = new Date();
const timestamp = now.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
});
// Add timestamp to the privacy screen title
const titleElement = document.querySelector('.priv-sub');
if (titleElement) {
titleElement.innerHTML += ` • Built: ${timestamp}`;
}
} catch (e) {
console.error('Error injecting build time or math art:', e);
}
}
// Call the function when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectBuildTime);
} else {
injectBuildTime();
}
</script>
</div>
<div class="header">
<h1>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
Studio Monitor <span>AI</span>
</h1>
<div class="controls">
<span class="count" id="count">0 images</span>
<button class="btn" onclick="setIntervalTime(30)">30s</button>
<button class="btn" onclick="setIntervalTime(120)">2m</button>
<button class="btn primary" onclick="refreshNow()">Refresh Now</button>
<button class="btn" id="archiveViewBtn" onclick="toggleArchiveView()" title="View archived items">📦 Archive</button>
<a href="trash.html" class="btn" title="View database inconsistencies" style="text-decoration:none;display:inline-flex;align-items:center;">🗑️ Trash</a>
<button class="btn" id="selectBtn" onclick="toggleSelectMode()">Select</button>
<button class="btn" id="privacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)">🔓</button>
</div>
<div class="status">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Auto-refresh: 2m</span>
</div>
<div class="upload-zone">
<input type="file" id="uploadInput" multiple onchange="handleUpload(event)" />
<button class="btn" onclick="document.getElementById('uploadInput').click()" title="Or paste image from clipboard (Ctrl+V)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
</svg>
Upload
</button>
</div>
</div>
<div class="gallery" id="gallery">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<h2>No images found</h2>
<p>Place this HTML file in your image folder, or configure the path below.</p>
</div>
</div>
<div class="toast" id="toast"></div>
<!-- Studio view (replaces old lightbox + faceswap modal) -->
<div id="studio" class="studio">
<!-- Topbar -->
<div class="studio-topbar">
<div class="studio-topbar-left">
<button class="btn" onclick="closeStudio()" style="font-size:11px;padding:4px 10px">✕ Gallery</button>
<button class="btn" id="lbBackBtn" onclick="jumpToPrevGroup()" style="font-size:11px;padding:4px 10px;display:none" title="Go back to previously opened group">↺ Back</button>
</div>
<div class="studio-topbar-center">
<input type="text" id="lbGroupNameInput"
style="background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#fff;font-size:13px;font-weight:600;padding:2px 5px;min-width:80px;max-width:200px;outline:none"
placeholder="Group name…"
onblur="updateGroupName()" onkeydown="if(event.key==='Enter')this.blur()"
title="Group name (applies to all images in this group)" />
<span id="lbPoseTag" style="font-size:11px;color:#f59e0b;display:none"></span>
<span id="lbImgDims" style="font-size:11px;color:#444;margin:0 8px"></span>
<span id="lbCounter" style="font-size:12px;color:#555"></span>
</div>
<div class="studio-topbar-right">
<button class="btn" id="sidebarToggleTopBtn" onclick="toggleSidebar()" style="font-size:11px;padding:4px 10px" title="Toggle toolbox">Tools</button>
</div>
</div>
<!-- Body: main viewer + sidebar -->
<div class="studio-body">
<!-- Main area: viewer + filmstrip -->
<div class="studio-main">
<div class="studio-viewer-row">
<button class="studio-nav-btn" id="lbPrev" onclick="lbNav(-1)"></button>
<div class="studio-viewer" id="studioViewer">
<div id="studioCheckerboard" style="display:none;position:absolute;pointer-events:none;z-index:1"></div>
<img id="lbImg" src="" alt="" onclick="toggleImageZoom(this)" />
<video id="lbVideo" style="display:none;max-width:100%;max-height:100%;border-radius:4px" controls autoplay muted loop></video>
<div class="lb-hidden-badge">Hidden from preview</div>
<div id="studioAngleBar"></div>
</div>
<button class="studio-nav-btn" id="lbNext" onclick="lbNav(1)"></button>
</div>
<!-- Film strip selector tabs -->
<div class="filmstrip-selector-tabs" style="display:flex;gap:4px;padding:4px 8px;background:#151515;border-top:1px solid #1a1a1a;align-items:center;justify-content:space-between;flex-shrink:0">
<div style="display:flex;gap:4px">
<button onclick="switchFilmstripTab('group')" class="fs-tab active" id="fsTabGroup" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'VISIBLE')">Group Variants</button>
<button onclick="switchFilmstripTab('hidden')" class="fs-tab" id="fsTabHidden" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'HIDDEN')">Hidden</button>
<button onclick="switchFilmstripTab('source')" class="fs-tab" id="fsTabSource" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'SOURCE')">Source</button>
<button onclick="switchFilmstripTab('archived')" class="fs-tab" id="fsTabArchived" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ARCHIVED')">Archived</button>
</div>
<div style="display:flex;gap:6px;align-items:center" id="multiSelectControls">
<button onclick="toggleMultiSelectMode()" id="multiSelectToggleBtn" style="background:#222;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer">Multi-select</button>
<div id="multiSelectActions" style="display:none;align-items:center;gap:4px">
<span id="multiSelectCount" style="font-size:11px;color:#aaa">0 selected</span>
<button onclick="bulkMoveSelected('VISIBLE')" class="fs-action-btn">Move to Group</button>
<button onclick="bulkMoveSelected('HIDDEN')" class="fs-action-btn">Move to Hidden</button>
<button onclick="bulkMoveSelected('SOURCE')" class="fs-action-btn">Move to Source</button>
<button onclick="bulkMoveSelected('ARCHIVED')" class="fs-action-btn">Move to Archived</button>
</div>
</div>
</div>
<!-- Film strip / variant thumbnails -->
<div class="studio-filmstrip" id="lbVariantStrip"></div>
<!-- Slideshow controls -->
<div id="slideshowBar" style="display:flex;align-items:center;gap:6px;padding:4px 8px;background:#111;border-top:1px solid #1a1a1a;flex-shrink:0">
<button onclick="toggleSlideshow()" id="slideshowBtn" style="background:none;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer">▶ Slideshow</button>
<button onclick="toggleOrbitFast()" id="lbOrbitFastBtn" style="background:none;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer" title="Loop current filmstrip at ~12fps">▶ Orbit</button>
<select id="slideshowInterval" onchange="updateSlideshowInterval()" style="background:#111;border:1px solid #333;color:#888;font-size:11px;padding:2px 4px;border-radius:4px">
<option value="2000">2s</option>
<option value="4000" selected>4s</option>
<option value="8000">8s</option>
<option value="15000">15s</option>
</select>
</div>
</div>
<!-- Sidebar toolbox -->
<div id="studioSidebar" class="studio-sidebar">
<button class="sidebar-toggle-btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox"></button>
<div id="sidebarInner" class="sidebar-inner">
<div class="sidebar-tabs">
<button class="sb-tab active" data-tab="info" onclick="switchSidebarTab('info')">Info</button>
<button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')">Generate</button>
<button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')">Face</button>
<button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')">Scene</button>
<button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')">SAM</button>
<button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit">Orbit</button>
</div>
<div class="sb-panels">
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
<div class="sb-panel active" id="sbPanelInfo">
<div class="sb-label">Image name</div>
<input type="text" class="sb-input" id="lbNameInput"
placeholder="Image name…"
onblur="updateName()" onkeydown="if(event.key==='Enter')this.blur()"
style="margin-bottom:8px">
<div id="lbClipDesc" class="sb-status" style="margin-bottom:6px;font-size:11px;color:#555"></div>
<div id="lbGenPromptWrap" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:3px">Generation prompt</div>
<div id="lbGenPrompt" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word"></div>
</div>
<div id="lbFaceBook" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:4px">Face reference</div>
<div style="display:flex;gap:8px;align-items:flex-start">
<img id="lbFaceThumb" style="width:72px;height:72px;object-fit:cover;border-radius:6px;border:1px solid #333;cursor:pointer;flex-shrink:0" onclick="window.open(this.src,'_blank')" title="Face crop — click to view full">
<button class="sb-btn" onclick="lbFindSimilarFaces()" title="Find groups with matching faces">Similar faces</button>
</div>
</div>
<div class="sb-sep"></div>
<div class="sb-label">Order & visibility</div>
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" id="lbMoveUp" style="display:none" onclick="lbMoveInGroup(-1)" title="Move earlier in group">↑ Earlier</button>
<button class="sb-btn" id="lbMoveDown" style="display:none" onclick="lbMoveInGroup(1)" title="Move later in group">↓ Later</button>
<button class="sb-btn" id="lbPreferredBtn" style="display:none;color:#f59e0b" onclick="lbSetPreferred()" title="Set as preferred reference">★ Preferred</button>
<button class="sb-btn" id="lbSourceBtn" onclick="toggleSourceKeybind()" title="Toggle mark as SOURCE (Shortcut: S)">★ Source</button>
<button class="sb-btn" id="lbEyeBtn" onclick="lbToggleHidden()" title="Hide/show in cycling">👁 Visibility</button>
<button class="sb-btn" id="lbEstimate21Btn" onclick="estimate21PlusKeybind()" title="Detect adult 21+ tags (Shortcut: E)">🔞 21+</button>
</div>
<div class="sb-sep"></div>
<div class="sb-label">Actions</div>
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
<button class="sb-btn purple" id="lbUndressBtn" style="display:none" onclick="lbUndress()">Undress</button>
<button class="sb-btn teal" id="lbFaceswapBtn" onclick="lbFaceswap()">Faceswap →</button>
</div>
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" id="lbExtract" style="display:none" onclick="extractCurrentImage()">Extract</button>
<button class="sb-btn" id="lbNoBgBtn" onclick="lbRemoveBg()">No BG</button>
<button class="sb-btn" id="lbInvertAlphaBtn" onclick="lbInvertAlpha()" title="Invert transparency — fixes when the wrong segment was kept">Invert α</button>
<button class="sb-btn" id="lbCropBtn" style="display:none" onclick="lbAutoCrop()" title="Crop away transparent border">Crop</button>
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group">Duplicate</button>
<button class="sb-btn" id="lbManualCropBtn" onclick="startManualCrop()" title="Drag to crop image">Crop…</button>
<button class="sb-btn" id="lbManualPadBtn" onclick="startManualPad()" title="Expand canvas by padding each side">Expand…</button>
<button class="sb-btn" id="lbRotateLeftBtn" onclick="lbRotate(-90)" title="Rotate 90° counter-clockwise">⟲ 90°</button>
<button class="sb-btn" id="lbRotateRightBtn" onclick="lbRotate(90)" title="Rotate 90° clockwise">⟳ 90°</button>
<button class="sb-btn" id="lbPoseBtn" onclick="lbTogglePose()" title="Preview body pose skeleton">Pose</button>
<button class="sb-btn" id="lbMakeVideoBtn" onclick="lbMakeVideo()" title="Stitch group images (or Shift+Click selection) into a short MP4">Make video</button>
<button class="sb-btn amber" id="lbOrbitBtn" onclick="lbOrbit()" title="Generate orbit preview (Fast=depth-card, Quality=Qwen turntable)">Orbit 3D<span id="orbitBadge" class="orbit-badge" style="display:none"></span></button>
<a class="sb-btn" id="lbDownloadBtn" download style="text-decoration:none">Download</a>
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b" title="Move to archive (recoverable)">Archive</button>
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" title="Click once to arm, then again to confirm">Delete</button>
</div>
<div id="lbSourceRefs" class="sb-source-refs" style="display:none"></div>
</div>
<!-- Other panels: rendered dynamically -->
<div class="sb-panel" id="sbPanelGenerate"></div>
<div class="sb-panel" id="sbPanelFaceswap"></div>
<div class="sb-panel" id="sbPanelScenery"></div>
<div class="sb-panel" id="sbPanelSegment"></div>
<div class="sb-panel" id="sbPanelOrbit"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Orbit preview modal -->
<div id="orbitModal">
<div class="orbit-box">
<button class="orbit-close" onclick="closeOrbitModal()" title="Close">&times;</button>
<div class="orbit-title">Orbit 3D preview</div>
<!-- === SETTINGS STATE === -->
<div id="orbitSettings">
<!-- Engine selector -->
<div class="orbit-engine-toggle">
<button id="orbitEngineDepth" class="active" onclick="orbitSetEngine('depth')">Fast — depth-card</button>
<button id="orbitEngineQwen" onclick="orbitSetEngine('qwen')">Quality — Qwen turntable</button>
</div>
<!-- Depth-card controls -->
<div id="orbitDepthControls" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:6px">
<label style="font-size:11px;color:#666;display:flex;align-items:center;gap:6px">
Parallax
<input type="range" id="orbitParallaxSlider" min="2" max="20" value="8" step="1"
style="width:80px;accent-color:#f59e0b"
oninput="document.getElementById('orbitParallaxVal').textContent=this.value+'%'">
<span id="orbitParallaxVal">8%</span>
</label>
<label style="font-size:11px;color:#666;display:flex;align-items:center;gap:6px">
Mode
<select id="orbitModeSelect" style="background:#1a1a1a;border:1px solid #333;color:#ccc;font-size:11px;padding:2px 4px;border-radius:4px">
<option value="swing" selected>Swing</option>
<option value="orbit">Full 360°</option>
</select>
</label>
</div>
<!-- Qwen turntable controls -->
<div id="orbitQwenControls" class="orbit-qwen-controls">
<label style="font-size:11px;color:#666;display:flex;align-items:center;gap:6px">
Views
<select id="orbitNviewsSelect" style="background:#1a1a1a;border:1px solid #333;color:#ccc;font-size:11px;padding:2px 4px;border-radius:4px">
<option value="12">12 (fast ~4 min)</option>
<option value="24" selected>24 (smooth ~9 min)</option>
</select>
</label>
<label style="font-size:11px;color:#666;display:flex;align-items:center;gap:6px">
Quality
<select id="orbitStepsSelect" style="background:#1a1a1a;border:1px solid #333;color:#ccc;font-size:11px;padding:2px 4px;border-radius:4px">
<option value="4">4 steps (draft)</option>
<option value="8" selected>8 steps (good)</option>
</select>
</label>
</div>
<div id="orbitQwenCacheStatus" style="font-size:10px;color:#f59e0b;margin-bottom:6px;display:none"></div>
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center">
<button class="sb-btn amber" id="orbitGenerateBtn" onclick="orbitGenerate()">Generate</button>
<button class="sb-btn" id="orbitLoadCacheBtn" onclick="orbitLoadCache()" style="display:none">Load cached ✓</button>
</div>
<div id="turntableBgStatus" style="display:none;margin-top:8px">
<div class="tbs-dot" id="tbsDot"></div>
<span id="tbsText"></span>
</div>
</div>
<!-- === PLAYER STATE === -->
<div id="orbitPlayer" style="display:none">
<div id="orbitStatus" class="orbit-status" style="margin-bottom:8px">Ready</div>
<!-- Frame flipper (shows transparency via checkerboard bg) -->
<div class="orbit-player" id="orbitFlipperWrap">
<div class="orbit-img-wrap">
<img id="orbitFlipperImg" alt="orbit frame">
</div>
<div class="orbit-player-controls">
<button class="sb-btn" onclick="orbitFlipperToggle()" id="orbitFlipperPlayBtn">⏸ Pause</button>
<span class="orbit-frame-counter" id="orbitFrameCounter">0/0</span>
<label style="font-size:11px;color:#666;display:flex;align-items:center;gap:6px;flex-shrink:0">
Speed
<input type="range" id="orbitFpsSlider" min="1" max="30" value="1" step="1"
style="width:60px;accent-color:#f59e0b"
oninput="_orbitSetFps(+this.value);document.getElementById('orbitFpsVal').textContent=this.value+'fps'">
<span id="orbitFpsVal" style="min-width:30px">1fps</span>
</label>
</div>
</div>
<!-- Video fallback (depth-card MP4 — no transparency) -->
<video id="orbitVideo" controls loop autoplay muted playsinline style="width:100%;border-radius:6px;display:none"></video>
<div class="orbit-actions" style="margin-top:10px">
<button class="sb-btn" onclick="orbitDownloadFrames()" id="orbitDlFramesBtn" style="display:none">⬇ Frames</button>
<button class="sb-btn" onclick="orbitDownload()" id="orbitDlBtn" style="display:none">⬇ MP4</button>
<button class="sb-btn" onclick="orbitBackToSettings()" style="margin-left:auto">⚙ Settings</button>
</div>
<div id="orbitNobgHint" style="display:none;margin-top:8px;font-size:11px;color:#78716c;background:#1c1917;padding:6px 10px;border-radius:6px">
Tip: run <strong style="color:#d4a27a">No BG</strong> first — transparent background gives much better depth separation.
</div>
</div>
</div>
</div>
<div class="batch-bar" id="batchBar">
<div id="poseMenu" class="pose-menu"></div>
<span class="batch-count" id="batchCount">0 selected</span>
<div class="batch-prompt-wrap">
<input class="batch-prompt" id="batchPromptInput" list="promptHistory"
placeholder="temporal prompt..."
onkeydown="if(event.key==='Enter')processSelected()" />
<datalist id="promptHistory"></datalist>
</div>
<button class="btn" id="poseBtn" onclick="togglePoseMenu()">Poses</button>
<button class="btn" onclick="selectAll()">All</button>
<button class="btn" onclick="deselectAll()">None</button>
<button class="btn" id="groupBtn" onclick="groupSelected()">Group</button>
<button class="btn" id="multiRefBtn" onclick="processMultiRef()" style="display:none" title="Use selected (23) as combined references → single output">Multi-ref</button>
<span id="multiRefLegend" style="display:none;font-size:10px;color:#888;white-space:nowrap"></span>
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
<span class="batch-progress" id="batchProgress"></span>
<button class="btn" onclick="toggleSelectMode()">Done</button>
</div>
<div class="variant-picker" id="variantPicker" onclick="if(event.target===this)closeVariantPicker()">
<div class="variant-picker-inner">
<div class="variant-picker-title" id="variantPickerTitle">Select reference image</div>
<div class="variant-picker-grid" id="variantPickerGrid"></div>
<div class="variant-picker-actions">
<button class="btn danger" id="variantPickerDeselect" style="display:none" onclick="deselectGroupFromPicker()">Deselect</button>
<button class="btn" onclick="closeVariantPicker()">Cancel</button>
</div>
</div>
</div>
<script>
// ============================================
// CONFIGURATION - EDIT THIS SECTION
// ============================================
// Set this to your image folder path relative to this HTML file
// Examples: './' (same folder), './images/', '../screenshots/'
const IMAGE_FOLDER = './';
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"_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_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.png",
"_turntable/cg_7ec17537/views/20260628_043045_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042836_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042836_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042239_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042228_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042217_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042206_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042154_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042143_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042132_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042121_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042109_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042058_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042047_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042036_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042025_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042014_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_042002_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041951_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041940_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041929_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041918_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041906_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041855_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041649_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041638_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041627_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041615_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041604_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041553_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041542_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041531_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041520_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041508_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041457_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041446_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041435_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041424_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041413_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041401_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041206_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041206_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_041119_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_040812_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_040437_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_035637_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
"_turntable/cg_7ec17537/views/20260628_035637_pad_20260628_035257_dup_20260628_033600_dup_view_000_000deg.png",
"_turntable/cg_7ec17537/views/20260628_035257_dup_20260628_033600_dup_view_000_000deg.png",
"_turntable/cg_7ec17537/views/20260628_034809_dup_20260628_033600_dup_view_000_000deg.png",
"_turntable/cg_7ec17537/views/20260628_034614_dup_20260628_033600_dup_view_000_000deg.png",
"20260618_053802_image.png_face.png",
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.nobg.png",
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.png",
"pass-1.png_face.png",
"20260628_031641_dup_20260628_031119_pad_pass-1.nobg.png",
"20260628_031119_pad_pass-1.nobg.png",
"20260628_031316_pad_pass-1.png",
"20260628_031119_pad_pass-1.png",
"pa01.png_face.png",
"20260628_030838_pa01.png",
"20260628_030808_pa01.png",
"20260628_030624_pa01.png",
"20260628_030532_pa01.png",
"20260628_030241_pa01.png",
"20260628_030115_pa01.png",
"kk563t.png_face.png",
"20260628_025504_pad_pass-1.png",
"up_e25ad102_face.png",
"20260628_020853_pad_20260628_015651_img_12.nobg.png",
"20260628_020642_pad_20260628_015651_img_12.nobg.png",
"20260628_020610_pad_20260628_015651_img_12.nobg.png",
"20260628_020546_pad_20260628_015651_img_12.nobg.png",
"20260628_020519_pad_20260628_015651_img_12.nobg.png",
"20260628_020428_pad_20260628_015651_img_12.nobg.png",
"20260628_020302_pad_20260628_015651_img_12.nobg.png",
"20260628_020125_pad_20260628_015651_img_12.nobg.png",
"20260628_015815_pad_20260628_015651_img_12.nobg.png",
"20260628_015832_pad_20260628_015651_img_12.png",
"20260628_015815_pad_20260628_015651_img_12.png",
"20260628_015707_img_12.png",
"20260628_015651_img_12.png",
"20260615_153426_img_12.png",
"20260628_015423_img_12.png",
"cg_5991f3f2_face.png",
"20260628_012806_pad_20260624_143523_Screenshot_From_2026-06-17_07-41-27.nobg.nobg.png",
"20260628_014614_p10.nobg.png",
"p10.png_face.png",
"20260628_014614_p10.png",
"20260628_014603_p10.png",
"up_2687be75_face.png",
"20260628_013521_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
"20260628_013511_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
"20260628_013459_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
"20260628_013429_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
"20260628_013429_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260628_013412_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260628_012832_pad_20260624_143523_Screenshot_From_2026-06-17_07-41-27.nobg.nobg.png",
"_turntable/cg_5991f3f2/views/view_022_330deg.png",
"20260628_011700_pad_20260624_154318_img_72.png",
"20260628_012006_pad_20260624_154318_img_72.png",
"20260628_011837_pad_20260624_154318_img_72.png",
"20260628_012240_crop_20260628_011837_pad_20260624_154318_img_72.png",
"20260628_011227_dup_Pasted image (9).png",
"20260628_011118_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
"20260628_010957_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
"20260628_010947_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
"20260628_010858_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
"20260628_010858_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
"20260628_010841_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
"20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
"20260628_010630_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
"20260628_010408_image.png",
"20260627_203626_image.png",
"up_7b6eaabb_face.png",
"cg_809653a0_face.png",
"bb_01.png_face.png",
"_turntable/up_aa1add4d/views/view_023_345deg.png",
"_turntable/up_aa1add4d/views/view_003_045deg.png",
"_turntable/up_aa1add4d/views/view_001_015deg.png",
"_turntable/up_7b6eaabb/views/view_001_015deg.png",
"_turntable/up_7b6eaabb/views/view_000_000deg.png",
"_turntable/up_6ed4293c/views/view_023_345deg.png",
"_turntable/up_6ed4293c/views/view_022_330deg.png",
"_turntable/up_6ed4293c/views/view_021_315deg.png",
"_turntable/up_6ed4293c/views/view_020_300deg.png",
"_turntable/up_6ed4293c/views/view_019_285deg.png",
"_turntable/up_6ed4293c/views/view_018_270deg.png",
"_turntable/up_6ed4293c/views/view_017_255deg.png",
"_turntable/up_6ed4293c/views/view_016_240deg.png",
"_turntable/up_6ed4293c/views/view_015_225deg.png",
"_turntable/up_6ed4293c/views/view_014_210deg.png",
"_turntable/up_6ed4293c/views/view_013_195deg.png",
"_turntable/up_6ed4293c/views/view_012_180deg.png",
"_turntable/up_6ed4293c/views/view_011_165deg.png",
"_turntable/up_6ed4293c/views/view_010_150deg.png",
"_turntable/up_6ed4293c/views/view_009_135deg.png",
"_turntable/up_6ed4293c/views/view_008_120deg.png",
"_turntable/up_6ed4293c/views/view_007_105deg.png",
"_turntable/up_6ed4293c/views/view_006_090deg.png",
"_turntable/up_6ed4293c/views/view_005_075deg.png",
"_turntable/up_6ed4293c/views/view_004_060deg.png",
"_turntable/up_6ed4293c/views/view_003_045deg.png",
"_turntable/up_6ed4293c/views/view_002_030deg.png",
"_turntable/up_6ed4293c/views/view_001_015deg.png",
"_turntable/up_6ed4293c/views/view_000_000deg.png",
"20260628_001216_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232502_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232843_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232526_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232302_pad_20260627_232133_image.png",
"20260627_232238_pad_20260627_232133_image.png",
"up_6ed4293c_face.png",
"20260627_232209_image.png",
"20260627_232151_image.png",
"20260627_232133_image.png",
"20260627_230405_mr_image.png",
"20260627_222806_mr_image.png",
"20260627_230301_7_20260627_201343_image.nobg.png",
"20260627_201629_7_20260627_201343_image.nobg.png",
"20260627_201629_7_20260627_201343_image.png",
"20260627_225051_mr_image.png",
"20260627_224340_mr_image.png",
"20260627_224013_mr_image.png",
"20260627_223316_image.png",
"20260627_223304_image.png",
"20260627_223252_image.png",
"20260627_223028_image.png",
"20260627_223017_image.png",
"20260627_223005_image.png",
"20260627_222926_image.png",
"20260627_222915_image.png",
"20260627_222904_image.png",
"20260627_222321_image.png",
"20260627_222310_image.png",
"20260627_222259_image.png",
"20260627_222112_mr_image.png",
"20260627_222047_mr_image.png",
"20260627_222015_mr_image.png",
"20260626_034949_pose_jb.nobg.png",
"cg_0ed9a15a_face.png",
"_turntable/cg_7c7b22c9/views/view_022_330deg.png",
"20260618_015124_20260618_010617_20260615_153426_img_12.nobg.png",
"_turntable/cg_5991f3f2/views/view_002_030deg.png",
"20260627_213910_best_02_person_0.874_2026-06-27_12-18-49.png",
"20260627_213840_best_02_person_0.874_2026-06-27_12-18-49.png",
"20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260627_213756_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260627_213743_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260627_213727_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_213711_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_213658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_213626_best_02_person_0.874_2026-06-27_12-18-49.png",
"20260627_213613_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260627_213559_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_213517_image.png",
"20260627_213504_image.png",
"20260627_213451_image.png",
"20260627_213241_12_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_213227_11_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"_turntable/cg_74544975/views/view_023_345deg.png",
"20260627_213215_10_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"_turntable/cg_74544975/views/view_022_330deg.png",
"_turntable/cg_74544975/views/view_021_315deg.png",
"_turntable/cg_74544975/views/view_020_300deg.png",
"_turntable/cg_74544975/views/view_019_285deg.png",
"_turntable/cg_74544975/views/view_018_270deg.png",
"20260627_213202_9_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_213149_8_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"_turntable/cg_74544975/views/view_004_060deg.png",
"_turntable/cg_74544975/views/view_001_015deg.png",
"_turntable/cg_74544975/views/view_000_000deg.png",
"20260627_213102_12_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_213037_11_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_213013_10_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212949_9_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212937_3_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_212925_8_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212913_2_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_212901_7_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212848_1_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_212836_6_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212824_0_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_212814_best_02_person_0.874_2026-06-27_20-05-05.png",
"20260627_212814_best_02_person_0.874_2026-06-27_20-05-05.jpg",
"20260627_212814_best_01_person_0.874_2026-06-27_20-05-05.png",
"20260627_212814_best_00_person_0.874_2026-06-27_20-05-05.png",
"20260627_212814_best_00_person_0.874_2026-06-27_20-05-05.jpg",
"20260627_212812_5_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg",
"20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212748_3_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212735_2_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212726_best_02_person_0.874_2026-06-27_12-18-49.png",
"20260627_212726_best_01_person_0.874_2026-06-27_12-18-49.png",
"20260627_212726_best_01_person_0.874_2026-06-27_12-18-49.jpg",
"20260627_212726_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212726_best_00_person_0.874_2026-06-27_12-18-49.jpg",
"20260627_212723_1_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212710_0_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_204815_image.png",
"20260627_204804_image.png",
"20260627_204756_image.png",
"_turntable/cg_f85e7ae1/views/view_023_345deg.png",
"_turntable/cg_f85e7ae1/views/view_013_195deg.png",
"_turntable/cg_f85e7ae1/views/view_012_180deg.png",
"_turntable/cg_f85e7ae1/views/view_010_150deg.png",
"_turntable/cg_f85e7ae1/views/view_001_015deg.png",
"_turntable/cg_f85e7ae1/views/view_000_000deg.png",
"_turntable/cg_75340e8e/views/view_017_255deg.png",
"_turntable/cg_75340e8e/views/view_016_240deg.png",
"_turntable/cg_75340e8e/views/view_014_210deg.png",
"_turntable/cg_75340e8e/views/view_013_195deg.png",
"_turntable/cg_75340e8e/views/view_012_180deg.png",
"_turntable/cg_75340e8e/views/view_011_165deg.png",
"_turntable/cg_75340e8e/views/view_010_150deg.png",
"_turntable/cg_75340e8e/views/view_009_135deg.png",
"_turntable/cg_75340e8e/views/view_008_120deg.png",
"_turntable/cg_75340e8e/views/view_007_105deg.png",
"_turntable/cg_75340e8e/views/view_023_345deg.png",
"_turntable/cg_75340e8e/views/view_001_015deg.png",
"_turntable/cg_75340e8e/views/view_000_000deg.png",
"20260627_203706_11_20260627_200330_image.png",
"20260627_203654_11_20260627_200330_image.png",
"20260627_203642_11_20260627_200330_image.png",
"20260627_203607_image.png",
"20260627_203549_image.png",
"20260627_203531_Screenshot_From_2026-06-27_20-04-11.png",
"20260627_203514_Screenshot_From_2026-06-27_20-04-11.png",
"20260627_203459_Screenshot_From_2026-06-27_20-04-11.png",
"20260627_203349_image.png",
"20260627_203337_image.png",
"20260627_203324_image.png",
"20260627_203312_image.png",
"20260627_203300_image.png",
"20260627_203247_image.png",
"up_c8be3aa7_face.png",
"20260627_203235_Screenshot_From_2026-06-27_19-59-43.png",
"20260627_203223_Screenshot_From_2026-06-27_19-59-43.png",
"20260627_203211_Screenshot_From_2026-06-27_19-59-43.png",
"20260627_203127_image.png",
"20260627_203105_Screenshot_From_2026-06-27_19-59-43.png",
"20260627_203054_Screenshot_From_2026-06-27_20-04-11.png",
"20260627_203044_image.png",
"20260627_203032_image.png",
"_turntable/up_c8be3aa7/views/view_002_030deg.png",
"_turntable/up_c8be3aa7/views/view_003_045deg.png",
"20260627_201807_12_20260627_201343_image.png",
"20260627_201753_Screenshot_From_2026-06-27_12-32-39.png",
"20260627_201751_11_20260627_201343_image.png",
"20260627_201733_10_20260627_201343_image.png",
"20260627_201711_9_20260627_201343_image.png",
"20260627_201648_8_20260627_201343_image.png",
"20260627_201622_Screenshot_From_2026-06-27_20-04-55.png",
"20260627_201614_Screenshot_From_2026-06-27_20-04-11.png",
"20260627_201558_Screenshot_From_2026-06-27_19-59-43.png",
"20260627_201548_5_20260627_201343_image.png",
"20260627_201518_image.png",
"20260627_201500_3_20260627_201343_image.png",
"20260627_201439_2_20260627_201343_image.png",
"20260627_201418_1_20260627_201343_image.png",
"20260627_201400_0_20260627_201343_image.png",
"20260627_201343_image.png",
"_turntable/up_c8be3aa7/views/view_007_105deg.png",
"_turntable/up_c8be3aa7/views/view_006_090deg.png",
"_turntable/up_c8be3aa7/views/view_005_075deg.png",
"_turntable/up_c8be3aa7/views/view_004_060deg.png",
"_turntable/up_c8be3aa7/views/view_001_015deg.png",
"20260627_200711_11_20260627_200330_image.png",
"20260627_200506_image.png",
"20260627_200422_2_20260627_200330_image.png",
"20260627_200414_image.png",
"20260627_200405_1_20260627_200330_image.png",
"20260627_200330_image.png",
"_turntable/up_c8be3aa7/views/view_000_000deg.png",
"20260627_200201_12_20260627_195835_image.png",
"20260627_200144_11_20260627_195835_image.png",
"20260627_200127_10_20260627_195835_image.png",
"20260627_200111_9_20260627_195835_image.png",
"20260627_200100_8_20260627_195835_image.png",
"20260627_200047_7_20260627_195835_image.png",
"20260627_200032_6_20260627_195835_image.png",
"20260627_200015_5_20260627_195835_image.png",
"20260627_200001_4_20260627_195835_image.png",
"20260627_195953_image.png",
"20260627_195945_3_20260627_195835_image.png",
"20260627_195929_2_20260627_195835_image.png",
"20260627_195915_1_20260627_195835_image.png",
"20260627_195858_0_20260627_195835_image.png",
"20260627_195835_image.png",
"_turntable/img_87.png/views/view_023_345deg.png",
"_turntable/img_87.png/views/view_022_330deg.png",
"_turntable/img_87.png/views/view_021_315deg.png",
"_turntable/img_87.png/views/view_020_300deg.png",
"_turntable/img_87.png/views/view_019_285deg.png",
"_turntable/img_87.png/views/view_018_270deg.png",
"_turntable/img_87.png/views/view_017_255deg.png",
"_turntable/img_87.png/views/view_016_240deg.png",
"_turntable/img_87.png/views/view_015_225deg.png",
"_turntable/up_aa1add4d/views/view_022_330deg.png",
"_turntable/up_aa1add4d/views/view_021_315deg.png",
"_turntable/up_aa1add4d/views/view_020_300deg.png",
"_turntable/up_aa1add4d/views/view_019_285deg.png",
"_turntable/up_aa1add4d/views/view_018_270deg.png",
"_turntable/up_aa1add4d/views/view_017_255deg.png",
"_turntable/up_aa1add4d/views/view_016_240deg.png",
"_turntable/up_aa1add4d/views/view_015_225deg.png",
"_turntable/up_aa1add4d/views/view_014_210deg.png",
"_turntable/up_aa1add4d/views/view_013_195deg.png",
"_turntable/up_aa1add4d/views/view_012_180deg.png",
"_turntable/up_aa1add4d/views/view_011_165deg.png",
"_turntable/up_aa1add4d/views/view_010_150deg.png",
"_turntable/up_aa1add4d/views/view_009_135deg.png",
"_turntable/up_aa1add4d/views/view_008_120deg.png",
"_turntable/up_aa1add4d/views/view_007_105deg.png",
"_turntable/up_aa1add4d/views/view_006_090deg.png",
"_turntable/up_aa1add4d/views/view_005_075deg.png",
"_turntable/up_aa1add4d/views/view_004_060deg.png",
"_turntable/up_aa1add4d/views/view_002_030deg.png",
"_turntable/up_aa1add4d/views/view_000_000deg.png",
"_turntable/up_ae825fce/views/view_023_345deg.png",
"_turntable/up_ae825fce/views/view_022_330deg.png",
"_turntable/up_ae825fce/views/view_021_315deg.png",
"_turntable/up_ae825fce/views/view_020_300deg.png",
"_turntable/up_ae825fce/views/view_019_285deg.png",
"_turntable/up_ae825fce/views/view_018_270deg.png",
"_turntable/up_ae825fce/views/view_017_255deg.png",
"_turntable/up_ae825fce/views/view_016_240deg.png",
"_turntable/up_ae825fce/views/view_015_225deg.png",
"_turntable/up_ae825fce/views/view_014_210deg.png",
"_turntable/up_ae825fce/views/view_013_195deg.png",
"_turntable/up_ae825fce/views/view_012_180deg.png",
"_turntable/up_ae825fce/views/view_011_165deg.png",
"_turntable/up_ae825fce/views/view_010_150deg.png",
"_turntable/up_ae825fce/views/view_009_135deg.png",
"_turntable/up_ae825fce/views/view_008_120deg.png",
"_turntable/up_ae825fce/views/view_007_105deg.png",
"_turntable/up_ae825fce/views/view_006_090deg.png",
"_turntable/up_ae825fce/views/view_005_075deg.png",
"_turntable/up_ae825fce/views/view_004_060deg.png",
"_turntable/up_ae825fce/views/view_003_045deg.png",
"_turntable/up_ae825fce/views/view_002_030deg.png",
"_turntable/up_ae825fce/views/view_001_015deg.png",
"_turntable/up_ae825fce/views/view_000_000deg.png",
"_turntable/up_78ae5271/views/view_023_345deg.png",
"_turntable/up_78ae5271/views/view_022_330deg.png",
"_turntable/up_78ae5271/views/view_021_315deg.png",
"_turntable/up_78ae5271/views/view_020_300deg.png",
"_turntable/up_78ae5271/views/view_019_285deg.png",
"_turntable/up_78ae5271/views/view_018_270deg.png",
"_turntable/up_78ae5271/views/view_017_255deg.png",
"_turntable/up_78ae5271/views/view_016_240deg.png",
"_turntable/up_78ae5271/views/view_015_225deg.png",
"_turntable/up_78ae5271/views/view_014_210deg.png",
"_turntable/up_78ae5271/views/view_013_195deg.png",
"_turntable/up_78ae5271/views/view_012_180deg.png",
"_turntable/up_78ae5271/views/view_011_165deg.png",
"_turntable/up_78ae5271/views/view_010_150deg.png",
"_turntable/up_78ae5271/views/view_009_135deg.png",
"_turntable/up_78ae5271/views/view_008_120deg.png",
"_turntable/up_78ae5271/views/view_007_105deg.png",
"_turntable/up_78ae5271/views/view_006_090deg.png",
"_turntable/up_78ae5271/views/view_005_075deg.png",
"_turntable/up_78ae5271/views/view_004_060deg.png",
"_turntable/up_78ae5271/views/view_003_045deg.png",
"_turntable/up_78ae5271/views/view_002_030deg.png",
"_turntable/up_78ae5271/views/view_001_015deg.png",
"_turntable/up_78ae5271/views/view_000_000deg.png",
"_turntable/cg_4e2b6449/views/view_023_345deg.png",
"_turntable/cg_4e2b6449/views/view_022_330deg.png",
"_turntable/cg_4e2b6449/views/view_021_315deg.png",
"_turntable/cg_4e2b6449/views/view_020_300deg.png",
"_turntable/cg_4e2b6449/views/view_019_285deg.png",
"_turntable/cg_4e2b6449/views/view_018_270deg.png",
"_turntable/cg_4e2b6449/views/view_017_255deg.png",
"_turntable/cg_4e2b6449/views/view_016_240deg.png",
"_turntable/cg_4e2b6449/views/view_015_225deg.png",
"_turntable/cg_4e2b6449/views/view_014_210deg.png",
"_turntable/cg_4e2b6449/views/view_013_195deg.png",
"_turntable/cg_4e2b6449/views/view_012_180deg.png",
"_turntable/cg_4e2b6449/views/view_011_165deg.png",
"_turntable/cg_4e2b6449/views/view_010_150deg.png",
"_turntable/cg_4e2b6449/views/view_009_135deg.png",
"_turntable/cg_4e2b6449/views/view_008_120deg.png",
"_turntable/cg_4e2b6449/views/view_007_105deg.png",
"_turntable/cg_4e2b6449/views/view_006_090deg.png",
"_turntable/cg_4e2b6449/views/view_005_075deg.png",
"_turntable/cg_4e2b6449/views/view_004_060deg.png",
"_turntable/up_5b23ff92/views/view_023_345deg.png",
"_turntable/up_5b23ff92/views/view_022_330deg.png",
"_turntable/up_5b23ff92/views/view_021_315deg.png",
"_turntable/up_5b23ff92/views/view_020_300deg.png",
"_turntable/up_5b23ff92/views/view_019_285deg.png",
"_turntable/up_5b23ff92/views/view_018_270deg.png",
"_turntable/up_5b23ff92/views/view_017_255deg.png",
"_turntable/cg_6f321af3/views/view_023_345deg.png",
"_turntable/cg_6f321af3/views/view_022_330deg.png",
"_turntable/cg_6f321af3/views/view_021_315deg.png",
"_turntable/cg_6f321af3/views/view_020_300deg.png",
"_turntable/cg_6f321af3/views/view_019_285deg.png",
"_turntable/cg_6f321af3/views/view_018_270deg.png",
"_turntable/cg_6f321af3/views/view_017_255deg.png",
"_turntable/cg_6f321af3/views/view_016_240deg.png",
"_turntable/cg_6f321af3/views/view_015_225deg.png",
"_turntable/cg_6f321af3/views/view_014_210deg.png",
"_turntable/cg_6f321af3/views/view_013_195deg.png",
"_turntable/cg_6f321af3/views/view_012_180deg.png",
"_turntable/cg_6f321af3/views/view_011_165deg.png",
"_turntable/cg_6f321af3/views/view_010_150deg.png",
"_turntable/cg_6f321af3/views/view_009_135deg.png",
"_turntable/cg_6f321af3/views/view_008_120deg.png",
"_turntable/cg_6f321af3/views/view_007_105deg.png",
"_turntable/cg_6f321af3/views/view_006_090deg.png",
"_turntable/cg_6f321af3/views/view_005_075deg.png",
"_turntable/cg_6f321af3/views/view_004_060deg.png",
"_turntable/cg_6f321af3/views/view_003_045deg.png",
"_turntable/cg_6f321af3/views/view_002_030deg.png",
"_turntable/cg_6f321af3/views/view_001_015deg.png",
"_turntable/cg_6f321af3/views/view_000_000deg.png",
"_turntable/up_1cff7d63/views/view_023_345deg.png",
"_turntable/up_1cff7d63/views/view_022_330deg.png",
"_turntable/up_1cff7d63/views/view_021_315deg.png",
"_turntable/up_1cff7d63/views/view_020_300deg.png",
"_turntable/up_1cff7d63/views/view_019_285deg.png",
"_turntable/up_1cff7d63/views/view_018_270deg.png",
"_turntable/up_1cff7d63/views/view_017_255deg.png",
"_turntable/up_1cff7d63/views/view_016_240deg.png",
"_turntable/up_1cff7d63/views/view_015_225deg.png",
"_turntable/up_1cff7d63/views/view_014_210deg.png",
"_turntable/up_1cff7d63/views/view_013_195deg.png",
"_turntable/up_1cff7d63/views/view_012_180deg.png",
"_turntable/up_1cff7d63/views/view_011_165deg.png",
"_turntable/up_1cff7d63/views/view_010_150deg.png",
"_turntable/up_1cff7d63/views/view_009_135deg.png",
"_turntable/up_1cff7d63/views/view_008_120deg.png",
"_turntable/up_1cff7d63/views/view_007_105deg.png",
"_turntable/up_1cff7d63/views/view_006_090deg.png",
"_turntable/up_1cff7d63/views/view_005_075deg.png",
"_turntable/up_1cff7d63/views/view_004_060deg.png",
"_turntable/up_1cff7d63/views/view_003_045deg.png",
"_turntable/up_1cff7d63/views/view_002_030deg.png",
"_turntable/up_1cff7d63/views/view_001_015deg.png",
"_turntable/up_1cff7d63/views/view_000_000deg.png",
"_turntable/cg_e964fb19/views/view_023_345deg.png",
"_turntable/cg_e964fb19/views/view_022_330deg.png",
"_turntable/cg_e964fb19/views/view_021_315deg.png",
"_turntable/cg_e964fb19/views/view_020_300deg.png",
"_turntable/cg_e964fb19/views/view_019_285deg.png",
"_turntable/cg_e964fb19/views/view_018_270deg.png",
"_turntable/cg_e964fb19/views/view_017_255deg.png",
"_turntable/cg_e964fb19/views/view_016_240deg.png",
"_turntable/cg_e964fb19/views/view_015_225deg.png",
"_turntable/cg_e964fb19/views/view_014_210deg.png",
"_turntable/cg_e964fb19/views/view_013_195deg.png",
"_turntable/cg_e964fb19/views/view_012_180deg.png",
"_turntable/cg_e964fb19/views/view_011_165deg.png",
"_turntable/cg_e964fb19/views/view_010_150deg.png",
"_turntable/cg_e964fb19/views/view_009_135deg.png",
"_turntable/cg_e964fb19/views/view_008_120deg.png",
"_turntable/cg_e964fb19/views/view_007_105deg.png",
"_turntable/cg_e964fb19/views/view_006_090deg.png",
"_turntable/cg_e964fb19/views/view_005_075deg.png",
"_turntable/cg_e964fb19/views/view_004_060deg.png",
"_turntable/cg_e964fb19/views/view_003_045deg.png",
"_turntable/cg_e964fb19/views/view_002_030deg.png",
"_turntable/cg_e964fb19/views/view_001_015deg.png",
"_turntable/cg_e964fb19/views/view_000_000deg.png",
"_turntable/cg_75340e8e/views/view_022_330deg.png",
"_turntable/cg_75340e8e/views/view_021_315deg.png",
"_turntable/cg_75340e8e/views/view_020_300deg.png",
"_turntable/cg_75340e8e/views/view_019_285deg.png",
"_turntable/cg_75340e8e/views/view_018_270deg.png",
"_turntable/cg_75340e8e/views/view_015_225deg.png",
"_turntable/cg_75340e8e/views/view_006_090deg.png",
"_turntable/cg_75340e8e/views/view_005_075deg.png",
"_turntable/cg_75340e8e/views/view_004_060deg.png",
"_turntable/cg_75340e8e/views/view_003_045deg.png",
"_turntable/cg_75340e8e/views/view_002_030deg.png",
"_turntable/cg_f85e7ae1/views/view_022_330deg.png",
"_turntable/cg_f85e7ae1/views/view_021_315deg.png",
"_turntable/cg_f85e7ae1/views/view_020_300deg.png",
"_turntable/cg_f85e7ae1/views/view_019_285deg.png",
"_turntable/cg_f85e7ae1/views/view_018_270deg.png",
"_turntable/cg_f85e7ae1/views/view_017_255deg.png",
"_turntable/cg_f85e7ae1/views/view_016_240deg.png",
"_turntable/cg_f85e7ae1/views/view_015_225deg.png",
"_turntable/cg_f85e7ae1/views/view_014_210deg.png",
"_turntable/cg_f85e7ae1/views/view_011_165deg.png",
"_turntable/cg_f85e7ae1/views/view_009_135deg.png",
"_turntable/cg_f85e7ae1/views/view_008_120deg.png",
"_turntable/cg_f85e7ae1/views/view_007_105deg.png",
"_turntable/cg_f85e7ae1/views/view_006_090deg.png",
"_turntable/cg_f85e7ae1/views/view_005_075deg.png",
"_turntable/cg_f85e7ae1/views/view_004_060deg.png",
"_turntable/cg_f85e7ae1/views/view_003_045deg.png",
"_turntable/cg_f85e7ae1/views/view_002_030deg.png",
"_turntable/up_c31bb439/views/view_023_345deg.png",
"_turntable/up_c31bb439/views/view_022_330deg.png",
"_turntable/up_c31bb439/views/view_021_315deg.png",
"_turntable/up_c31bb439/views/view_020_300deg.png",
"_turntable/up_c31bb439/views/view_019_285deg.png",
"_turntable/up_c31bb439/views/view_018_270deg.png",
"_turntable/up_c31bb439/views/view_017_255deg.png",
"_turntable/up_c31bb439/views/view_016_240deg.png",
"_turntable/up_c31bb439/views/view_015_225deg.png",
"_turntable/up_c31bb439/views/view_014_210deg.png",
"_turntable/up_c31bb439/views/view_013_195deg.png",
"_turntable/up_c31bb439/views/view_012_180deg.png",
"_turntable/up_c31bb439/views/view_011_165deg.png",
"_turntable/up_c31bb439/views/view_010_150deg.png",
"_turntable/up_c31bb439/views/view_009_135deg.png",
"_turntable/up_c31bb439/views/view_008_120deg.png",
"_turntable/up_c31bb439/views/view_007_105deg.png",
"_turntable/up_c31bb439/views/view_006_090deg.png",
"_turntable/up_c31bb439/views/view_005_075deg.png",
"_turntable/up_c31bb439/views/view_004_060deg.png",
"_turntable/up_c31bb439/views/view_003_045deg.png",
"_turntable/up_c31bb439/views/view_002_030deg.png",
"_turntable/up_c31bb439/views/view_001_015deg.png",
"_turntable/up_c31bb439/views/view_000_000deg.png",
"_turntable/cg_4671b6c4/views/view_023_345deg.png",
"_turntable/cg_4671b6c4/views/view_022_330deg.png",
"_turntable/cg_4671b6c4/views/view_021_315deg.png",
"_turntable/cg_4671b6c4/views/view_020_300deg.png",
"_turntable/cg_4671b6c4/views/view_019_285deg.png",
"_turntable/cg_4671b6c4/views/view_018_270deg.png",
"_turntable/cg_4671b6c4/views/view_017_255deg.png",
"_turntable/cg_4671b6c4/views/view_016_240deg.png",
"_turntable/cg_4671b6c4/views/view_015_225deg.png",
"_turntable/cg_4671b6c4/views/view_014_210deg.png",
"_turntable/cg_4671b6c4/views/view_013_195deg.png",
"_turntable/cg_4671b6c4/views/view_012_180deg.png",
"_turntable/cg_4671b6c4/views/view_011_165deg.png",
"_turntable/cg_4671b6c4/views/view_010_150deg.png",
"_turntable/cg_4671b6c4/views/view_009_135deg.png",
"_turntable/cg_4671b6c4/views/view_008_120deg.png",
"_turntable/cg_4671b6c4/views/view_007_105deg.png",
"_turntable/cg_4671b6c4/views/view_006_090deg.png",
"_turntable/cg_4671b6c4/views/view_005_075deg.png",
"_turntable/cg_4671b6c4/views/view_004_060deg.png",
"_turntable/cg_4671b6c4/views/view_003_045deg.png",
"_turntable/cg_4671b6c4/views/view_002_030deg.png",
"_turntable/cg_4671b6c4/views/view_001_015deg.png",
"_turntable/cg_4671b6c4/views/view_000_000deg.png",
"_turntable/cg_74544975/views/view_017_255deg.png",
"_turntable/cg_74544975/views/view_016_240deg.png",
"_turntable/cg_74544975/views/view_015_225deg.png",
"_turntable/cg_74544975/views/view_014_210deg.png",
"_turntable/cg_74544975/views/view_013_195deg.png",
"_turntable/cg_74544975/views/view_012_180deg.png",
"_turntable/cg_74544975/views/view_011_165deg.png",
"_turntable/cg_74544975/views/view_010_150deg.png",
"_turntable/cg_74544975/views/view_009_135deg.png",
"_turntable/cg_74544975/views/view_008_120deg.png",
"_turntable/cg_74544975/views/view_007_105deg.png",
"_turntable/cg_74544975/views/view_006_090deg.png",
"_turntable/cg_74544975/views/view_005_075deg.png",
"_turntable/cg_74544975/views/view_003_045deg.png",
"_turntable/cg_74544975/views/view_002_030deg.png",
"_turntable/cg_73e141fa/views/view_023_345deg.png",
"_turntable/cg_73e141fa/views/view_022_330deg.png",
"_turntable/cg_73e141fa/views/view_021_315deg.png",
"_turntable/cg_73e141fa/views/view_020_300deg.png",
"_turntable/cg_73e141fa/views/view_019_285deg.png",
"_turntable/cg_73e141fa/views/view_018_270deg.png",
"_turntable/cg_73e141fa/views/view_017_255deg.png",
"_turntable/cg_73e141fa/views/view_016_240deg.png",
"_turntable/cg_73e141fa/views/view_015_225deg.png",
"_turntable/cg_73e141fa/views/view_014_210deg.png",
"_turntable/cg_73e141fa/views/view_013_195deg.png",
"_turntable/cg_73e141fa/views/view_012_180deg.png",
"_turntable/cg_73e141fa/views/view_011_165deg.png",
"_turntable/cg_73e141fa/views/view_010_150deg.png",
"_turntable/cg_73e141fa/views/view_009_135deg.png",
"_turntable/cg_73e141fa/views/view_008_120deg.png",
"_turntable/cg_73e141fa/views/view_007_105deg.png",
"_turntable/cg_73e141fa/views/view_006_090deg.png",
"_turntable/cg_73e141fa/views/view_005_075deg.png",
"_turntable/cg_73e141fa/views/view_004_060deg.png",
"_turntable/cg_73e141fa/views/view_003_045deg.png",
"_turntable/cg_73e141fa/views/view_002_030deg.png",
"_turntable/cg_73e141fa/views/view_001_015deg.png",
"_turntable/cg_73e141fa/views/view_000_000deg.png",
"_turntable/img_51.png/views/view_023_345deg.png",
"_turntable/img_51.png/views/view_022_330deg.png",
"_turntable/img_51.png/views/view_021_315deg.png",
"_turntable/img_51.png/views/view_020_300deg.png",
"_turntable/img_51.png/views/view_019_285deg.png",
"_turntable/img_51.png/views/view_018_270deg.png",
"_turntable/img_51.png/views/view_017_255deg.png",
"_turntable/img_51.png/views/view_016_240deg.png",
"_turntable/img_51.png/views/view_015_225deg.png",
"_turntable/img_51.png/views/view_014_210deg.png",
"_turntable/img_51.png/views/view_013_195deg.png",
"_turntable/img_51.png/views/view_012_180deg.png",
"_turntable/img_51.png/views/view_011_165deg.png",
"_turntable/img_51.png/views/view_010_150deg.png",
"_turntable/img_51.png/views/view_009_135deg.png",
"_turntable/img_51.png/views/view_008_120deg.png",
"_turntable/img_51.png/views/view_007_105deg.png",
"_turntable/img_51.png/views/view_006_090deg.png",
"_turntable/img_51.png/views/view_005_075deg.png",
"_turntable/img_51.png/views/view_004_060deg.png",
"_turntable/img_51.png/views/view_003_045deg.png",
"_turntable/img_51.png/views/view_002_030deg.png",
"_turntable/img_51.png/views/view_001_015deg.png",
"_turntable/img_51.png/views/view_000_000deg.png",
"_turntable/up_455c085f/views/view_023_345deg.png",
"_turntable/up_455c085f/views/view_022_330deg.png",
"_turntable/up_455c085f/views/view_021_315deg.png",
"_turntable/up_455c085f/views/view_020_300deg.png",
"_turntable/up_455c085f/views/view_019_285deg.png",
"_turntable/up_455c085f/views/view_018_270deg.png",
"_turntable/up_455c085f/views/view_017_255deg.png",
"_turntable/up_455c085f/views/view_016_240deg.png",
"_turntable/up_455c085f/views/view_015_225deg.png",
"_turntable/up_455c085f/views/view_014_210deg.png",
"_turntable/up_455c085f/views/view_013_195deg.png",
"_turntable/up_455c085f/views/view_012_180deg.png",
"_turntable/up_455c085f/views/view_011_165deg.png",
"_turntable/up_455c085f/views/view_010_150deg.png",
"_turntable/up_455c085f/views/view_009_135deg.png",
"_turntable/up_455c085f/views/view_008_120deg.png",
"_turntable/up_455c085f/views/view_007_105deg.png",
"_turntable/up_455c085f/views/view_006_090deg.png",
"_turntable/up_455c085f/views/view_005_075deg.png",
"_turntable/up_455c085f/views/view_004_060deg.png",
"_turntable/up_455c085f/views/view_003_045deg.png",
"_turntable/up_455c085f/views/view_002_030deg.png",
"_turntable/up_455c085f/views/view_001_015deg.png",
"_turntable/up_455c085f/views/view_000_000deg.png",
"20260627_161604_image.nobg.png",
"20260627_161342_image.nobg.png",
"20260618_053458_image.png_face.png",
"20260627_161342_image.png",
"20260627_161217_pad_20260627_161055_image.png",
"20260627_161055_image.png",
"20260627_160901_mr_sc_image.png",
"20260627_160718_mr_sc_image.png",
"20260627_160551_mr_sc_image.png",
"20260627_160407_mr_sc_image.png",
"20260627_160228_mr_sc_image.png",
"20260627_160117_mr_sc_image.png",
"20260627_155655_sc_image.png",
"20260627_155122_20150913_211324.jpg",
"20260627_155049_20150913_211324.jpg",
"20260627_154946_20150913_211324.jpg",
"20260627_154852_20150913_211324.jpg",
"20260620_031100_20150913_211324.jpg",
"20260627_154745_20150913_211324.jpg",
"20260627_154404_20150913_211324.jpg",
"20260627_153734_pad_20260620_031100_20150913_211324.jpg",
"20260625_050554_sc_3_20260618_173728_image.nobg.png",
"_turntable/cg_ad73335e/views/view_022_330deg.png",
"_turntable/cg_ad73335e/views/view_021_315deg.png",
"_turntable/cg_ad73335e/views/view_020_300deg.png",
"_turntable/cg_ad73335e/views/view_019_285deg.png",
"_turntable/cg_ad73335e/views/view_002_030deg.png",
"_turntable/cg_ad73335e/views/view_001_015deg.png",
"cg_ac6274f0_face.png",
"20260618_180939_image.png_face.png",
"img_51.png_face.png",
"20260627_150448_pad_20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
"20260627_150351_pad_20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
"20260627_150251_pad_20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
"20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
"20260627_150207_pad_20260627_145719_img_51.png",
"20260627_150013_img_51.png",
"20260627_145730_img_51.png",
"20260627_145719_img_51.png",
"20260627_145706_mr_img_51.png",
"20260627_145436_image.png",
"_turntable/cg_f9537c3f/views/view_023_345deg.png",
"_turntable/cg_f9537c3f/views/view_001_015deg.png",
"_turntable/cg_f9537c3f/views/view_000_000deg.png",
"_turntable/up_98c8443e/views/view_023_345deg.png",
"_turntable/up_98c8443e/views/view_000_000deg.png",
"up_7382d00e_face.png",
"up_455c085f_face.png",
"_turntable/20150913_211324.jpg/views/view_012_180deg.png",
"_turntable/20150913_211324.jpg/views/view_013_195deg.png",
"_turntable/20150913_211324.jpg/views/view_014_210deg.png",
"_turntable/20150913_211324.jpg/views/view_016_240deg.png",
"_turntable/20150913_211324.jpg/views/view_021_315deg.png",
"_turntable/20150913_211324.jpg/views/view_003_045deg.png",
"_turntable/20150913_211324.jpg/views/view_011_165deg.png",
"_turntable/20150913_211324.jpg/views/view_010_150deg.png",
"20260618_182810_image.png_face.png",
"20260619_124210_1_20260619_123958_image.nobg.png",
"20260620_011328_mr_1_20260619_124038_image.nobg.png",
"20260619_124258_2_20260619_124038_image.nobg.png",
"20260619_124210_1_20260619_123958_image.png",
"20260620_011328_mr_1_20260619_124038_image.png",
"20260620_011153_mr_1_20260619_124038_image.png",
"20260622_180912_image.png",
"20260620_011137_mr_1_20260619_124038_image.png",
"20260620_011120_mr_1_20260619_124038_image.png",
"20260620_010925_mr_1_20260619_124038_image.png",
"20260620_010911_mr_1_20260619_124038_image.png",
"20260622_181407_0_20260619_124038_image.png",
"20260619_184150_2_20260619_184116_image.png",
"20260619_124242_2_20260619_123958_image.png",
"20260620_010559_1_20260619_124038_image.png",
"20260624_020723_image.png",
"20260624_020455_image.png",
"20260624_015653_image.png",
"20260624_011605_image.png",
"20260624_010045_7_20260624_005912_image.png",
"_turntable/b1.png/views/view_018_270deg.png",
"_turntable/b1.png/views/view_017_255deg.png",
"20260618_182106_image.png_face.png",
"20260622_175657_4_20260622_175551_image.png",
"20260622_175712_5_20260622_175551_image.png",
"_turntable/cg_2afca8fc/views/view_001_015deg.png",
"_turntable/cg_2afca8fc/views/view_016_240deg.png",
"_turntable/cg_2afca8fc/views/view_013_195deg.png",
"_turntable/cg_2afca8fc/views/view_009_135deg.png",
"_turntable/cg_2afca8fc/views/view_008_120deg.png",
"up_ae825fce_face.png",
"_turntable/cg_4e2b6449/views/view_000_000deg.png",
"_turntable/cg_4e2b6449/views/view_001_015deg.png",
"20260627_134506_pad_20260627_130837_image.png",
"20260627_134409_pad_20260627_130837_image.png",
"_turntable/cg_4e2b6449/views/view_003_045deg.png",
"_turntable/cg_4e2b6449/views/view_002_030deg.png",
"20260627_133359_pad_20260627_132332_image.nobg.png",
"20260627_133236_pad_20260627_132332_image.nobg.png",
"20260627_133149_pad_20260627_132332_image.nobg.png",
"20260627_133128_pad_20260627_132332_image.nobg.png",
"20260627_133024_pad_20260627_132332_image.nobg.png",
"20260627_132913_pad_20260627_132332_image.nobg.png",
"20260627_132744_pad_20260627_132332_image.nobg.png",
"20260627_132456_pad_20260627_132332_image.nobg.png",
"20260627_132402_pad_20260627_132332_image.nobg.png",
"20260627_132402_pad_20260627_132332_image.png",
"20260627_132332_image.png",
"_turntable/cg_65f71c4d/views/view_001_015deg.png",
"_turntable/cg_65f71c4d/views/view_000_000deg.png",
"_turntable/cg_65f71c4d/views/view_023_345deg.png",
"20260627_132208_pad_20260626_224632_sc_image.png",
"20260627_132151_sc_image.png",
"20260627_132035_pad_20260626_224632_sc_image.png",
"_turntable/cg_870f3f93/views/view_023_345deg.png",
"_turntable/cg_870f3f93/views/view_018_270deg.png",
"_turntable/cg_870f3f93/views/view_016_240deg.png",
"_turntable/cg_870f3f93/views/view_015_225deg.png",
"_turntable/cg_870f3f93/views/view_014_210deg.png",
"_turntable/cg_870f3f93/views/view_013_195deg.png",
"_turntable/cg_870f3f93/views/view_012_180deg.png",
"_turntable/cg_870f3f93/views/view_011_165deg.png",
"_turntable/cg_870f3f93/views/view_010_150deg.png",
"_turntable/cg_870f3f93/views/view_009_135deg.png",
"_turntable/cg_870f3f93/views/view_008_120deg.png",
"_turntable/cg_870f3f93/views/view_005_075deg.png",
"_turntable/cg_870f3f93/views/view_004_060deg.png",
"_turntable/cg_870f3f93/views/view_003_045deg.png",
"_turntable/cg_870f3f93/views/view_001_015deg.png",
"_turntable/cg_870f3f93/views/view_000_000deg.png",
"20260627_131628_pad_20260622_053758_2_20260621_115215_image.png",
"20260627_131605_pad_20260622_053758_2_20260621_115215_image.png",
"20260627_131420_2_20260621_115215_image.png",
"20260627_131120_pad_20260627_130837_image.png",
"20260627_131008_pad_20260627_130837_image.png",
"20260627_130925_pad_20260627_130837_image.png",
"20260627_130837_image.png",
"20260627_130827_image.png",
"_turntable/20260618_133729_image.png/views/view_021_315deg.png",
"_turntable/20260618_133729_image.png/views/view_020_300deg.png",
"_turntable/20260618_133729_image.png/views/view_017_255deg.png",
"_turntable/20260618_133729_image.png/views/view_005_075deg.png",
"_turntable/20260618_133729_image.png/views/view_004_060deg.png",
"_turntable/20260618_133729_image.png/views/view_003_045deg.png",
"_turntable/cg_3942c16b/views/view_007_105deg.png",
"_turntable/up_a1dddf6f/views/view_007_105deg.png",
"_turntable/up_a1dddf6f/views/view_009_135deg.png",
"_turntable/up_a1dddf6f/views/view_008_120deg.png",
"_turntable/up_a1dddf6f/views/view_023_345deg.png",
"20260625_014722_sc_mr_mr_image.nobg.png",
"_turntable/up_a1dddf6f/views/view_022_330deg.png",
"20260625_014604_sc_mr_mr_image.nobg.png",
"_turntable/up_a1dddf6f/views/view_021_315deg.png",
"_turntable/up_a1dddf6f/views/view_020_300deg.png",
"_turntable/up_a1dddf6f/views/view_019_285deg.png",
"_turntable/up_a1dddf6f/views/view_018_270deg.png",
"_turntable/up_a1dddf6f/views/view_015_225deg.png",
"_turntable/up_a1dddf6f/views/view_016_240deg.png",
"_turntable/up_a1dddf6f/views/view_014_210deg.png",
"_turntable/up_a1dddf6f/views/view_013_195deg.png",
"_turntable/up_a1dddf6f/views/view_011_165deg.png",
"_turntable/up_a1dddf6f/views/view_010_150deg.png",
"_turntable/up_a1dddf6f/views/view_004_060deg.png",
"_turntable/up_a1dddf6f/views/view_003_045deg.png",
"_turntable/up_a1dddf6f/views/view_002_030deg.png",
"_turntable/up_a1dddf6f/views/view_001_015deg.png",
"_turntable/up_a1dddf6f/views/view_000_000deg.png",
"_turntable/20260618_182106_image.png/views/view_002_030deg.png",
"_turntable/20260618_182106_image.png/views/view_007_105deg.png",
"_turntable/20260618_182106_image.png/views/view_008_120deg.png",
"_turntable/20260618_182106_image.png/views/view_010_150deg.png",
"_turntable/20260618_182106_image.png/views/view_011_165deg.png",
"_turntable/20260618_182106_image.png/views/view_022_330deg.png",
"_turntable/up_f9a5bb33/views/view_023_345deg.png",
"_turntable/up_f9a5bb33/views/view_001_015deg.png",
"_turntable/up_f9a5bb33/views/view_000_000deg.png",
"_turntable/cg_fb1043d8/views/view_001_015deg.png",
"_turntable/cg_fb1043d8/views/view_000_000deg.png",
"_turntable/cg_fb1043d8/views/view_023_345deg.png",
"_turntable/img_80.png/views/view_023_345deg.png",
"_turntable/img_80.png/views/view_001_015deg.png",
"_turntable/img_80.png/views/view_000_000deg.png",
"_turntable/cg_58c41add/views/view_017_255deg.png",
"_turntable/img_31.png/views/view_023_345deg.png",
"_turntable/img_31.png/views/view_016_240deg.png",
"_turntable/img_31.png/views/view_015_225deg.png",
"_turntable/img_31.png/views/view_014_210deg.png",
"_turntable/img_31.png/views/view_013_195deg.png",
"_turntable/img_31.png/views/view_012_180deg.png",
"_turntable/img_31.png/views/view_011_165deg.png",
"_turntable/img_31.png/views/view_010_150deg.png",
"_turntable/img_31.png/views/view_009_135deg.png",
"_turntable/img_31.png/views/view_008_120deg.png",
"_turntable/img_31.png/views/view_002_030deg.png",
"_turntable/img_31.png/views/view_001_015deg.png",
"_turntable/img_31.png/views/view_000_000deg.png",
"_turntable/img_87.png/views/view_014_210deg.png",
"_turntable/img_87.png/views/view_013_195deg.png",
"_turntable/img_87.png/views/view_012_180deg.png",
"_turntable/img_87.png/views/view_010_150deg.png",
"_turntable/img_87.png/views/view_009_135deg.png",
"_turntable/img_87.png/views/view_001_015deg.png",
"_turntable/img_87.png/views/view_000_000deg.png",
"_turntable/cg_ed2e43d1/views/view_002_030deg.png",
"_turntable/cg_ed2e43d1/views/view_001_015deg.png",
"_turntable/cg_ed2e43d1/views/view_000_000deg.png",
"_turntable/cg_ed2e43d1/views/view_023_345deg.png",
"_turntable/cg_ed2e43d1/views/view_022_330deg.png",
"_turntable/cg_ed2e43d1/views/view_018_270deg.png",
"_turntable/cg_ed2e43d1/views/view_017_255deg.png",
"_turntable/cg_ed2e43d1/views/view_016_240deg.png",
"_turntable/cg_ed2e43d1/views/view_015_225deg.png",
"_turntable/cg_ed2e43d1/views/view_014_210deg.png",
"_turntable/cg_ed2e43d1/views/view_013_195deg.png",
"_turntable/cg_ed2e43d1/views/view_012_180deg.png",
"_turntable/cg_ed2e43d1/views/view_011_165deg.png",
"_turntable/cg_ed2e43d1/views/view_010_150deg.png",
"_turntable/cg_ed2e43d1/views/view_009_135deg.png",
"_turntable/cg_ed2e43d1/views/view_008_120deg.png",
"_turntable/cg_ed2e43d1/views/view_007_105deg.png",
"20260627_124455_image.png",
"20260627_124123_12_20260627_123731_image.png",
"20260627_124106_11_20260627_123731_image.png",
"20260627_124049_image.png",
"20260627_124033_10_20260627_123731_image.png",
"20260627_124018_image.png",
"20260627_124001_9_20260627_123731_image.png",
"20260627_123945_8_20260627_123731_image.png",
"20260627_123935_image.png",
"20260627_123928_7_20260627_123731_image.png",
"20260627_123911_6_20260627_123731_image.png",
"20260627_123855_5_20260627_123731_image.png",
"20260627_123838_4_20260627_123731_image.png",
"20260627_123822_3_20260627_123731_image.png",
"20260627_123806_2_20260627_123731_image.png",
"20260627_123753_1_20260627_123731_image.png",
"20260627_123742_0_20260627_123731_image.png",
"20260627_123731_image.png",
"20260627_123721_12_20260627_123259_image.png",
"20260627_123710_11_20260627_123259_image.png",
"20260627_123659_10_20260627_123259_image.png",
"20260627_123648_9_20260627_123259_image.png",
"20260627_123636_8_20260627_123259_image.png",
"20260627_123619_7_20260627_123259_image.png",
"20260627_123603_6_20260627_123259_image.png",
"20260627_123546_5_20260627_123259_image.png",
"20260627_123530_4_20260627_123259_image.png",
"20260627_123519_3_20260627_123259_image.png",
"20260627_123507_2_20260627_123259_image.png",
"20260627_123456_1_20260627_123259_image.png",
"20260627_123445_0_20260627_123259_image.png",
"20260627_123259_image.png",
"_turntable/img_87.png/views/view_011_165deg.png",
"_turntable/img_87.png/views/view_008_120deg.png",
"_turntable/img_87.png/views/view_007_105deg.png",
"_turntable/img_87.png/views/view_006_090deg.png",
"_turntable/img_87.png/views/view_005_075deg.png",
"_turntable/img_87.png/views/view_004_060deg.png",
"_turntable/img_87.png/views/view_003_045deg.png",
"_turntable/img_87.png/views/view_002_030deg.png",
"_turntable/img_31.png/views/view_022_330deg.png",
"_turntable/img_31.png/views/view_021_315deg.png",
"_turntable/img_31.png/views/view_020_300deg.png",
"_turntable/img_31.png/views/view_019_285deg.png",
"_turntable/img_31.png/views/view_018_270deg.png",
"_turntable/img_31.png/views/view_017_255deg.png",
"_turntable/img_31.png/views/view_007_105deg.png",
"_turntable/img_31.png/views/view_006_090deg.png",
"_turntable/img_31.png/views/view_005_075deg.png",
"_turntable/img_31.png/views/view_004_060deg.png",
"_turntable/img_31.png/views/view_003_045deg.png",
"_turntable/cg_58c41add/views/view_023_345deg.png",
"_turntable/cg_58c41add/views/view_022_330deg.png",
"_turntable/cg_58c41add/views/view_021_315deg.png",
"_turntable/cg_58c41add/views/view_020_300deg.png",
"_turntable/cg_58c41add/views/view_019_285deg.png",
"_turntable/cg_58c41add/views/view_018_270deg.png",
"_turntable/cg_58c41add/views/view_016_240deg.png",
"_turntable/cg_58c41add/views/view_015_225deg.png",
"_turntable/cg_58c41add/views/view_014_210deg.png",
"_turntable/cg_58c41add/views/view_013_195deg.png",
"_turntable/cg_58c41add/views/view_012_180deg.png",
"_turntable/cg_58c41add/views/view_011_165deg.png",
"_turntable/cg_58c41add/views/view_010_150deg.png",
"_turntable/cg_58c41add/views/view_009_135deg.png",
"_turntable/cg_58c41add/views/view_008_120deg.png",
"_turntable/cg_58c41add/views/view_007_105deg.png",
"_turntable/cg_58c41add/views/view_006_090deg.png",
"_turntable/cg_58c41add/views/view_005_075deg.png",
"_turntable/cg_58c41add/views/view_004_060deg.png",
"_turntable/cg_58c41add/views/view_003_045deg.png",
"_turntable/cg_58c41add/views/view_002_030deg.png",
"_turntable/cg_58c41add/views/view_001_015deg.png",
"_turntable/cg_58c41add/views/view_000_000deg.png",
"_turntable/img_80.png/views/view_022_330deg.png",
"_turntable/img_80.png/views/view_021_315deg.png",
"_turntable/img_80.png/views/view_020_300deg.png",
"_turntable/img_80.png/views/view_019_285deg.png",
"_turntable/img_80.png/views/view_018_270deg.png",
"_turntable/img_80.png/views/view_017_255deg.png",
"_turntable/img_80.png/views/view_016_240deg.png",
"_turntable/img_80.png/views/view_015_225deg.png",
"_turntable/img_80.png/views/view_014_210deg.png",
"_turntable/img_80.png/views/view_013_195deg.png",
"_turntable/img_80.png/views/view_012_180deg.png",
"_turntable/img_80.png/views/view_011_165deg.png",
"_turntable/img_80.png/views/view_010_150deg.png",
"_turntable/img_80.png/views/view_009_135deg.png",
"_turntable/img_80.png/views/view_008_120deg.png",
"_turntable/img_80.png/views/view_007_105deg.png",
"_turntable/img_80.png/views/view_006_090deg.png",
"_turntable/img_80.png/views/view_005_075deg.png",
"_turntable/img_80.png/views/view_004_060deg.png",
"_turntable/img_80.png/views/view_003_045deg.png",
"_turntable/img_80.png/views/view_002_030deg.png",
"_turntable/up_94204739/views/view_023_345deg.png",
"_turntable/up_94204739/views/view_022_330deg.png",
"_turntable/up_94204739/views/view_021_315deg.png",
"_turntable/up_94204739/views/view_020_300deg.png",
"_turntable/up_94204739/views/view_019_285deg.png",
"_turntable/up_94204739/views/view_018_270deg.png",
"_turntable/up_94204739/views/view_017_255deg.png",
"_turntable/up_94204739/views/view_016_240deg.png",
"_turntable/up_94204739/views/view_015_225deg.png",
"_turntable/up_94204739/views/view_014_210deg.png",
"_turntable/up_94204739/views/view_013_195deg.png",
"_turntable/up_94204739/views/view_012_180deg.png",
"_turntable/up_94204739/views/view_011_165deg.png",
"_turntable/up_94204739/views/view_010_150deg.png",
"_turntable/up_94204739/views/view_009_135deg.png",
"_turntable/up_94204739/views/view_008_120deg.png",
"_turntable/up_94204739/views/view_007_105deg.png",
"_turntable/up_94204739/views/view_006_090deg.png",
"_turntable/up_94204739/views/view_005_075deg.png",
"_turntable/up_94204739/views/view_004_060deg.png",
"_turntable/up_94204739/views/view_003_045deg.png",
"_turntable/up_94204739/views/view_002_030deg.png",
"_turntable/up_94204739/views/view_001_015deg.png",
"_turntable/up_94204739/views/view_000_000deg.png",
"_turntable/cg_fb1043d8/views/view_022_330deg.png",
"_turntable/cg_fb1043d8/views/view_021_315deg.png",
"_turntable/cg_fb1043d8/views/view_020_300deg.png",
"_turntable/cg_fb1043d8/views/view_019_285deg.png",
"_turntable/cg_fb1043d8/views/view_018_270deg.png",
"_turntable/cg_fb1043d8/views/view_017_255deg.png",
"_turntable/cg_fb1043d8/views/view_016_240deg.png",
"_turntable/cg_fb1043d8/views/view_015_225deg.png",
"_turntable/cg_fb1043d8/views/view_014_210deg.png",
"_turntable/cg_fb1043d8/views/view_013_195deg.png",
"_turntable/cg_fb1043d8/views/view_012_180deg.png",
"_turntable/cg_fb1043d8/views/view_011_165deg.png",
"_turntable/cg_fb1043d8/views/view_010_150deg.png",
"_turntable/cg_fb1043d8/views/view_009_135deg.png",
"_turntable/cg_fb1043d8/views/view_008_120deg.png",
"_turntable/cg_fb1043d8/views/view_007_105deg.png",
"_turntable/cg_fb1043d8/views/view_006_090deg.png",
"_turntable/cg_fb1043d8/views/view_005_075deg.png",
"_turntable/cg_fb1043d8/views/view_004_060deg.png",
"_turntable/cg_fb1043d8/views/view_003_045deg.png",
"_turntable/cg_fb1043d8/views/view_002_030deg.png",
"_turntable/20260618_182106_image.png/views/view_023_345deg.png",
"_turntable/20260618_182106_image.png/views/view_021_315deg.png",
"_turntable/20260618_182106_image.png/views/view_020_300deg.png",
"_turntable/20260618_182106_image.png/views/view_019_285deg.png",
"_turntable/20260618_182106_image.png/views/view_018_270deg.png",
"_turntable/20260618_182106_image.png/views/view_017_255deg.png",
"_turntable/20260618_182106_image.png/views/view_016_240deg.png",
"_turntable/20260618_182106_image.png/views/view_015_225deg.png",
"_turntable/20260618_182106_image.png/views/view_014_210deg.png",
"_turntable/20260618_182106_image.png/views/view_013_195deg.png",
"_turntable/20260618_182106_image.png/views/view_012_180deg.png",
"_turntable/20260618_182106_image.png/views/view_009_135deg.png",
"_turntable/20260618_182106_image.png/views/view_006_090deg.png",
"_turntable/20260618_182106_image.png/views/view_005_075deg.png",
"_turntable/20260618_182106_image.png/views/view_004_060deg.png",
"_turntable/20260618_182106_image.png/views/view_003_045deg.png",
"_turntable/20260618_182106_image.png/views/view_001_015deg.png",
"_turntable/20260618_182106_image.png/views/view_000_000deg.png",
"_turntable/up_a1dddf6f/views/view_017_255deg.png",
"_turntable/up_a1dddf6f/views/view_012_180deg.png",
"_turntable/up_a1dddf6f/views/view_006_090deg.png",
"_turntable/up_a1dddf6f/views/view_005_075deg.png",
"_turntable/up_52dede0c/views/view_023_345deg.png",
"_turntable/up_52dede0c/views/view_022_330deg.png",
"_turntable/up_52dede0c/views/view_021_315deg.png",
"_turntable/up_52dede0c/views/view_020_300deg.png",
"_turntable/up_52dede0c/views/view_019_285deg.png",
"_turntable/up_52dede0c/views/view_018_270deg.png",
"_turntable/up_52dede0c/views/view_017_255deg.png",
"_turntable/up_52dede0c/views/view_016_240deg.png",
"_turntable/up_52dede0c/views/view_015_225deg.png",
"_turntable/up_52dede0c/views/view_014_210deg.png",
"_turntable/up_52dede0c/views/view_013_195deg.png",
"_turntable/up_52dede0c/views/view_012_180deg.png",
"_turntable/up_52dede0c/views/view_011_165deg.png",
"_turntable/up_52dede0c/views/view_010_150deg.png",
"_turntable/up_52dede0c/views/view_009_135deg.png",
"_turntable/up_52dede0c/views/view_008_120deg.png",
"_turntable/up_52dede0c/views/view_007_105deg.png",
"_turntable/up_52dede0c/views/view_006_090deg.png",
"_turntable/up_52dede0c/views/view_005_075deg.png",
"_turntable/up_52dede0c/views/view_004_060deg.png",
"_turntable/up_52dede0c/views/view_003_045deg.png",
"_turntable/up_52dede0c/views/view_002_030deg.png",
"_turntable/up_52dede0c/views/view_001_015deg.png",
"_turntable/up_52dede0c/views/view_000_000deg.png",
"_turntable/up_f9a5bb33/views/view_022_330deg.png",
"_turntable/up_f9a5bb33/views/view_021_315deg.png",
"_turntable/up_f9a5bb33/views/view_020_300deg.png",
"_turntable/up_f9a5bb33/views/view_019_285deg.png",
"_turntable/up_f9a5bb33/views/view_018_270deg.png",
"_turntable/up_f9a5bb33/views/view_017_255deg.png",
"_turntable/up_f9a5bb33/views/view_016_240deg.png",
"_turntable/up_f9a5bb33/views/view_015_225deg.png",
"_turntable/up_f9a5bb33/views/view_014_210deg.png",
"_turntable/up_f9a5bb33/views/view_013_195deg.png",
"_turntable/up_f9a5bb33/views/view_012_180deg.png",
"_turntable/up_f9a5bb33/views/view_011_165deg.png",
"_turntable/up_f9a5bb33/views/view_010_150deg.png",
"_turntable/up_f9a5bb33/views/view_009_135deg.png",
"_turntable/up_f9a5bb33/views/view_008_120deg.png",
"_turntable/up_f9a5bb33/views/view_007_105deg.png",
"_turntable/up_f9a5bb33/views/view_006_090deg.png",
"_turntable/up_f9a5bb33/views/view_005_075deg.png",
"_turntable/up_f9a5bb33/views/view_004_060deg.png",
"_turntable/up_f9a5bb33/views/view_003_045deg.png",
"_turntable/up_f9a5bb33/views/view_002_030deg.png",
"_turntable/cg_3942c16b/views/view_023_345deg.png",
"_turntable/cg_3942c16b/views/view_022_330deg.png",
"_turntable/cg_3942c16b/views/view_021_315deg.png",
"_turntable/cg_3942c16b/views/view_020_300deg.png",
"_turntable/cg_3942c16b/views/view_019_285deg.png",
"_turntable/cg_3942c16b/views/view_018_270deg.png",
"_turntable/cg_3942c16b/views/view_017_255deg.png",
"_turntable/cg_3942c16b/views/view_016_240deg.png",
"_turntable/cg_3942c16b/views/view_015_225deg.png",
"_turntable/cg_3942c16b/views/view_014_210deg.png",
"_turntable/cg_3942c16b/views/view_013_195deg.png",
"_turntable/cg_3942c16b/views/view_012_180deg.png",
"_turntable/cg_3942c16b/views/view_011_165deg.png",
"_turntable/cg_3942c16b/views/view_010_150deg.png",
"_turntable/cg_3942c16b/views/view_009_135deg.png",
"_turntable/cg_3942c16b/views/view_008_120deg.png",
"_turntable/cg_3942c16b/views/view_006_090deg.png",
"_turntable/cg_3942c16b/views/view_005_075deg.png",
"_turntable/cg_3942c16b/views/view_004_060deg.png",
"_turntable/cg_3942c16b/views/view_003_045deg.png",
"_turntable/cg_3942c16b/views/view_002_030deg.png",
"_turntable/cg_3942c16b/views/view_001_015deg.png",
"_turntable/cg_3942c16b/views/view_000_000deg.png",
"_turntable/20260618_053458_image.png/views/view_023_345deg.png",
"_turntable/20260618_053458_image.png/views/view_022_330deg.png",
"_turntable/20260618_053458_image.png/views/view_021_315deg.png",
"_turntable/20260618_053458_image.png/views/view_020_300deg.png",
"_turntable/20260618_053458_image.png/views/view_019_285deg.png",
"_turntable/20260618_053458_image.png/views/view_018_270deg.png",
"_turntable/20260618_053458_image.png/views/view_017_255deg.png",
"_turntable/20260618_053458_image.png/views/view_016_240deg.png",
"_turntable/20260618_053458_image.png/views/view_015_225deg.png",
"_turntable/20260618_053458_image.png/views/view_014_210deg.png",
"_turntable/20260618_053458_image.png/views/view_013_195deg.png",
"_turntable/20260618_053458_image.png/views/view_012_180deg.png",
"_turntable/20260618_053458_image.png/views/view_011_165deg.png",
"_turntable/20260618_053458_image.png/views/view_010_150deg.png",
"_turntable/20260618_053458_image.png/views/view_009_135deg.png",
"_turntable/20260618_053458_image.png/views/view_008_120deg.png",
"_turntable/20260618_053458_image.png/views/view_007_105deg.png",
"_turntable/20260618_053458_image.png/views/view_006_090deg.png",
"_turntable/20260618_053458_image.png/views/view_005_075deg.png",
"_turntable/20260618_053458_image.png/views/view_004_060deg.png",
"_turntable/20260618_053458_image.png/views/view_003_045deg.png",
"_turntable/20260618_053458_image.png/views/view_002_030deg.png",
"_turntable/20260618_053458_image.png/views/view_001_015deg.png",
"_turntable/20260618_053458_image.png/views/view_000_000deg.png",
"_turntable/cg_7ec17537/views/view_023_345deg.png",
"_turntable/cg_7ec17537/views/view_022_330deg.png",
"_turntable/cg_7ec17537/views/view_021_315deg.png",
"_turntable/cg_7ec17537/views/view_020_300deg.png",
"_turntable/cg_7ec17537/views/view_019_285deg.png",
"_turntable/cg_7ec17537/views/view_018_270deg.png",
"_turntable/cg_7ec17537/views/view_017_255deg.png",
"_turntable/cg_7ec17537/views/view_016_240deg.png",
"_turntable/cg_7ec17537/views/view_015_225deg.png",
"_turntable/cg_7ec17537/views/view_014_210deg.png",
"_turntable/cg_7ec17537/views/view_013_195deg.png",
"_turntable/cg_7ec17537/views/view_012_180deg.png",
"_turntable/cg_7ec17537/views/view_011_165deg.png",
"_turntable/cg_7ec17537/views/view_010_150deg.png",
"_turntable/cg_7ec17537/views/view_009_135deg.png",
"_turntable/cg_7ec17537/views/view_008_120deg.png",
"_turntable/cg_7ec17537/views/view_007_105deg.png",
"_turntable/cg_7ec17537/views/view_006_090deg.png",
"_turntable/cg_7ec17537/views/view_005_075deg.png",
"_turntable/cg_7ec17537/views/view_004_060deg.png",
"_turntable/cg_7ec17537/views/view_003_045deg.png",
"_turntable/cg_7ec17537/views/view_002_030deg.png",
"_turntable/cg_7ec17537/views/view_001_015deg.png",
"_turntable/cg_7ec17537/views/view_000_000deg.png",
"_turntable/cg_7ec17537/views/20260628_033600_dup_view_000_000deg.png",
"_turntable/cg_7ec17537/views/20260628_033831_dup_20260628_033600_dup_view_000_000deg.png",
"_turntable/up_154b502c/views/view_023_345deg.png",
"_turntable/up_154b502c/views/view_022_330deg.png",
"_turntable/up_154b502c/views/view_021_315deg.png",
"_turntable/up_154b502c/views/view_020_300deg.png",
"_turntable/up_154b502c/views/view_019_285deg.png",
"_turntable/up_154b502c/views/view_018_270deg.png",
"_turntable/up_154b502c/views/view_017_255deg.png",
"_turntable/up_154b502c/views/view_016_240deg.png",
"_turntable/up_154b502c/views/view_015_225deg.png",
"_turntable/up_154b502c/views/view_014_210deg.png",
"_turntable/up_154b502c/views/view_013_195deg.png",
"_turntable/up_154b502c/views/view_012_180deg.png",
"_turntable/up_154b502c/views/view_011_165deg.png",
"_turntable/up_154b502c/views/view_010_150deg.png",
"_turntable/up_154b502c/views/view_009_135deg.png",
"_turntable/up_154b502c/views/view_008_120deg.png",
"_turntable/up_154b502c/views/view_007_105deg.png",
"_turntable/up_154b502c/views/view_006_090deg.png",
"_turntable/up_154b502c/views/view_005_075deg.png",
"_turntable/up_154b502c/views/view_004_060deg.png",
"_turntable/up_154b502c/views/view_003_045deg.png",
"_turntable/up_154b502c/views/view_002_030deg.png",
"_turntable/up_154b502c/views/view_001_015deg.png",
"_turntable/up_154b502c/views/view_000_000deg.png",
"_turntable/cg_36a33bae/views/view_023_345deg.png",
"_turntable/cg_36a33bae/views/view_022_330deg.png",
"_turntable/cg_36a33bae/views/view_021_315deg.png",
"_turntable/cg_36a33bae/views/view_020_300deg.png",
"_turntable/cg_36a33bae/views/view_019_285deg.png",
"_turntable/cg_36a33bae/views/view_018_270deg.png",
"_turntable/cg_36a33bae/views/view_017_255deg.png",
"_turntable/cg_36a33bae/views/view_016_240deg.png",
"_turntable/cg_36a33bae/views/view_015_225deg.png",
"_turntable/cg_36a33bae/views/view_014_210deg.png",
"_turntable/cg_36a33bae/views/view_013_195deg.png",
"_turntable/cg_36a33bae/views/view_012_180deg.png",
"_turntable/cg_36a33bae/views/view_011_165deg.png",
"_turntable/cg_36a33bae/views/view_010_150deg.png",
"_turntable/cg_36a33bae/views/view_009_135deg.png",
"_turntable/cg_36a33bae/views/view_008_120deg.png",
"_turntable/cg_36a33bae/views/view_007_105deg.png",
"_turntable/cg_36a33bae/views/view_006_090deg.png",
"_turntable/cg_36a33bae/views/view_005_075deg.png",
"_turntable/cg_36a33bae/views/view_004_060deg.png",
"_turntable/cg_36a33bae/views/view_003_045deg.png",
"_turntable/cg_36a33bae/views/view_002_030deg.png",
"_turntable/cg_36a33bae/views/view_001_015deg.png",
"_turntable/cg_36a33bae/views/view_000_000deg.png",
"_turntable/20260618_133729_image.png/views/view_023_345deg.png",
"_turntable/20260618_133729_image.png/views/view_022_330deg.png",
"_turntable/20260618_133729_image.png/views/view_019_285deg.png",
"_turntable/20260618_133729_image.png/views/view_018_270deg.png",
"_turntable/20260618_133729_image.png/views/view_016_240deg.png",
"_turntable/20260618_133729_image.png/views/view_015_225deg.png",
"_turntable/20260618_133729_image.png/views/view_014_210deg.png",
"_turntable/20260618_133729_image.png/views/view_013_195deg.png",
"_turntable/20260618_133729_image.png/views/view_012_180deg.png",
"_turntable/20260618_133729_image.png/views/view_011_165deg.png",
"_turntable/20260618_133729_image.png/views/view_010_150deg.png",
"_turntable/20260618_133729_image.png/views/view_009_135deg.png",
"_turntable/20260618_133729_image.png/views/view_008_120deg.png",
"_turntable/20260618_133729_image.png/views/view_007_105deg.png",
"_turntable/20260618_133729_image.png/views/view_006_090deg.png",
"_turntable/20260618_133729_image.png/views/view_002_030deg.png",
"_turntable/20260618_133729_image.png/views/view_001_015deg.png",
"_turntable/20260618_133729_image.png/views/view_000_000deg.png",
"_turntable/20260618_181715_image.png/views/view_023_345deg.png",
"_turntable/20260618_181715_image.png/views/view_022_330deg.png",
"_turntable/20260618_181715_image.png/views/view_021_315deg.png",
"_turntable/20260618_181715_image.png/views/view_020_300deg.png",
"_turntable/20260618_181715_image.png/views/view_019_285deg.png",
"_turntable/20260618_181715_image.png/views/view_018_270deg.png",
"_turntable/20260618_181715_image.png/views/view_017_255deg.png",
"_turntable/20260618_181715_image.png/views/view_016_240deg.png",
"_turntable/20260618_181715_image.png/views/view_015_225deg.png",
"_turntable/20260618_181715_image.png/views/view_014_210deg.png",
"_turntable/20260618_181715_image.png/views/view_013_195deg.png",
"_turntable/20260618_181715_image.png/views/view_012_180deg.png",
"_turntable/20260618_181715_image.png/views/view_011_165deg.png",
"_turntable/20260618_181715_image.png/views/view_010_150deg.png",
"_turntable/20260618_181715_image.png/views/view_009_135deg.png",
"_turntable/20260618_181715_image.png/views/view_008_120deg.png",
"_turntable/20260618_181715_image.png/views/view_007_105deg.png",
"_turntable/20260618_181715_image.png/views/view_006_090deg.png",
"_turntable/20260618_181715_image.png/views/view_005_075deg.png",
"_turntable/20260618_181715_image.png/views/view_004_060deg.png",
"_turntable/20260618_181715_image.png/views/view_003_045deg.png",
"_turntable/20260618_181715_image.png/views/view_002_030deg.png",
"_turntable/20260618_181715_image.png/views/view_001_015deg.png",
"_turntable/20260618_181715_image.png/views/view_000_000deg.png",
"_turntable/up_a1104f04/views/view_022_330deg.png",
"_turntable/up_a1104f04/views/view_021_315deg.png",
"_turntable/up_a1104f04/views/view_020_300deg.png",
"_turntable/up_a1104f04/views/view_019_285deg.png",
"_turntable/up_a1104f04/views/view_018_270deg.png",
"_turntable/up_a1104f04/views/view_017_255deg.png",
"_turntable/up_a1104f04/views/view_016_240deg.png",
"_turntable/up_a1104f04/views/view_015_225deg.png",
"_turntable/up_a1104f04/views/view_014_210deg.png",
"_turntable/up_a1104f04/views/view_013_195deg.png",
"_turntable/up_a1104f04/views/view_012_180deg.png",
"_turntable/up_a1104f04/views/view_011_165deg.png",
"_turntable/up_a1104f04/views/view_010_150deg.png",
"_turntable/up_a1104f04/views/view_009_135deg.png",
"_turntable/up_a1104f04/views/view_008_120deg.png",
"_turntable/up_a1104f04/views/view_007_105deg.png",
"_turntable/up_a1104f04/views/view_006_090deg.png",
"_turntable/up_a1104f04/views/view_005_075deg.png",
"_turntable/up_a1104f04/views/view_004_060deg.png",
"_turntable/up_a1104f04/views/view_003_045deg.png",
"_turntable/up_a1104f04/views/view_002_030deg.png",
"_turntable/up_a1104f04/views/view_001_015deg.png",
"_turntable/up_a1104f04/views/view_000_000deg.png",
"_turntable/cg_8c8d8ec9/views/view_023_345deg.png",
"_turntable/cg_8c8d8ec9/views/view_022_330deg.png",
"_turntable/cg_8c8d8ec9/views/view_021_315deg.png",
"_turntable/cg_8c8d8ec9/views/view_020_300deg.png",
"_turntable/cg_8c8d8ec9/views/view_019_285deg.png",
"_turntable/cg_8c8d8ec9/views/view_018_270deg.png",
"_turntable/cg_8c8d8ec9/views/view_017_255deg.png",
"_turntable/cg_8c8d8ec9/views/view_016_240deg.png",
"_turntable/cg_8c8d8ec9/views/view_015_225deg.png",
"_turntable/cg_8c8d8ec9/views/view_014_210deg.png",
"_turntable/cg_8c8d8ec9/views/view_013_195deg.png",
"_turntable/cg_8c8d8ec9/views/view_012_180deg.png",
"_turntable/cg_8c8d8ec9/views/view_011_165deg.png",
"_turntable/cg_8c8d8ec9/views/view_010_150deg.png",
"_turntable/cg_8c8d8ec9/views/view_009_135deg.png",
"_turntable/cg_8c8d8ec9/views/view_008_120deg.png",
"_turntable/cg_8c8d8ec9/views/view_007_105deg.png",
"_turntable/cg_8c8d8ec9/views/view_006_090deg.png",
"_turntable/cg_8c8d8ec9/views/view_005_075deg.png",
"_turntable/cg_8c8d8ec9/views/view_004_060deg.png",
"_turntable/cg_8c8d8ec9/views/view_003_045deg.png",
"_turntable/cg_8c8d8ec9/views/view_002_030deg.png",
"_turntable/cg_8c8d8ec9/views/view_001_015deg.png",
"_turntable/cg_8c8d8ec9/views/view_000_000deg.png",
"_turntable/cg_9231ec1c/views/view_023_345deg.png",
"_turntable/cg_9231ec1c/views/view_022_330deg.png",
"_turntable/cg_9231ec1c/views/view_021_315deg.png",
"_turntable/cg_9231ec1c/views/view_020_300deg.png",
"_turntable/cg_9231ec1c/views/view_019_285deg.png",
"_turntable/cg_9231ec1c/views/view_018_270deg.png",
"_turntable/cg_9231ec1c/views/view_017_255deg.png",
"_turntable/cg_9231ec1c/views/view_016_240deg.png",
"_turntable/cg_9231ec1c/views/view_015_225deg.png",
"_turntable/cg_9231ec1c/views/view_014_210deg.png",
"_turntable/cg_9231ec1c/views/view_013_195deg.png",
"_turntable/cg_9231ec1c/views/view_012_180deg.png",
"_turntable/cg_9231ec1c/views/view_011_165deg.png",
"_turntable/cg_9231ec1c/views/view_010_150deg.png",
"_turntable/cg_9231ec1c/views/view_009_135deg.png",
"_turntable/cg_9231ec1c/views/view_008_120deg.png",
"_turntable/cg_9231ec1c/views/view_007_105deg.png",
"_turntable/cg_9231ec1c/views/view_006_090deg.png",
"_turntable/cg_9231ec1c/views/view_005_075deg.png",
"_turntable/cg_9231ec1c/views/view_004_060deg.png",
"_turntable/cg_9231ec1c/views/view_003_045deg.png",
"_turntable/cg_9231ec1c/views/view_002_030deg.png",
"_turntable/cg_9231ec1c/views/view_001_015deg.png",
"_turntable/cg_9231ec1c/views/view_000_000deg.png",
"_turntable/20150913_211324.jpg/views/view_023_345deg.png",
"_turntable/20150913_211324.jpg/views/view_022_330deg.png",
"_turntable/20150913_211324.jpg/views/view_020_300deg.png",
"_turntable/20150913_211324.jpg/views/view_019_285deg.png",
"_turntable/20150913_211324.jpg/views/view_018_270deg.png",
"_turntable/20150913_211324.jpg/views/view_017_255deg.png",
"_turntable/20150913_211324.jpg/views/view_015_225deg.png",
"_turntable/20150913_211324.jpg/views/view_009_135deg.png",
"_turntable/20150913_211324.jpg/views/view_008_120deg.png",
"_turntable/20150913_211324.jpg/views/view_007_105deg.png",
"_turntable/20150913_211324.jpg/views/view_006_090deg.png",
"_turntable/20150913_211324.jpg/views/view_005_075deg.png",
"_turntable/20150913_211324.jpg/views/view_004_060deg.png",
"_turntable/20150913_211324.jpg/views/view_002_030deg.png",
"_turntable/20150913_211324.jpg/views/view_001_015deg.png",
"_turntable/20150913_211324.jpg/views/view_000_000deg.png",
"_turntable/20260618_182810_image.png/views/view_023_345deg.png",
"_turntable/20260618_182810_image.png/views/view_022_330deg.png",
"_turntable/20260618_182810_image.png/views/view_021_315deg.png",
"_turntable/20260618_182810_image.png/views/view_020_300deg.png",
"_turntable/20260618_182810_image.png/views/view_019_285deg.png",
"_turntable/20260618_182810_image.png/views/view_018_270deg.png",
"_turntable/20260618_182810_image.png/views/view_017_255deg.png",
"_turntable/20260618_182810_image.png/views/view_016_240deg.png",
"_turntable/20260618_182810_image.png/views/view_015_225deg.png",
"_turntable/20260618_182810_image.png/views/view_014_210deg.png",
"_turntable/20260618_182810_image.png/views/view_013_195deg.png",
"_turntable/20260618_182810_image.png/views/view_012_180deg.png",
"_turntable/20260618_182810_image.png/views/view_011_165deg.png",
"_turntable/20260618_182810_image.png/views/view_010_150deg.png",
"_turntable/20260618_182810_image.png/views/view_009_135deg.png",
"_turntable/20260618_182810_image.png/views/view_008_120deg.png",
"_turntable/20260618_182810_image.png/views/view_007_105deg.png",
"_turntable/20260618_182810_image.png/views/view_006_090deg.png",
"_turntable/20260618_182810_image.png/views/view_005_075deg.png",
"_turntable/20260618_182810_image.png/views/view_004_060deg.png",
"_turntable/20260618_182810_image.png/views/view_003_045deg.png",
"_turntable/20260618_182810_image.png/views/view_002_030deg.png",
"_turntable/20260618_182810_image.png/views/view_001_015deg.png",
"_turntable/20260618_182810_image.png/views/view_000_000deg.png",
"_turntable/up_8cf9e62e/views/view_023_345deg.png",
"_turntable/up_8cf9e62e/views/view_022_330deg.png",
"_turntable/up_8cf9e62e/views/view_021_315deg.png",
"_turntable/up_8cf9e62e/views/view_020_300deg.png",
"_turntable/up_8cf9e62e/views/view_019_285deg.png",
"_turntable/up_8cf9e62e/views/view_018_270deg.png",
"_turntable/up_8cf9e62e/views/view_017_255deg.png",
"_turntable/up_8cf9e62e/views/view_016_240deg.png",
"_turntable/up_8cf9e62e/views/view_015_225deg.png",
"_turntable/up_8cf9e62e/views/view_014_210deg.png",
"_turntable/up_8cf9e62e/views/view_013_195deg.png",
"_turntable/up_8cf9e62e/views/view_012_180deg.png",
"_turntable/up_8cf9e62e/views/view_011_165deg.png",
"_turntable/up_8cf9e62e/views/view_010_150deg.png",
"_turntable/up_8cf9e62e/views/view_009_135deg.png",
"_turntable/up_8cf9e62e/views/view_008_120deg.png",
"_turntable/up_8cf9e62e/views/view_007_105deg.png",
"_turntable/up_8cf9e62e/views/view_006_090deg.png",
"_turntable/up_8cf9e62e/views/view_005_075deg.png",
"_turntable/up_8cf9e62e/views/view_004_060deg.png",
"_turntable/up_8cf9e62e/views/view_003_045deg.png",
"_turntable/up_8cf9e62e/views/view_002_030deg.png",
"_turntable/up_8cf9e62e/views/view_001_015deg.png",
"_turntable/up_8cf9e62e/views/view_000_000deg.png",
"_turntable/cg_870f3f93/views/view_022_330deg.png",
"_turntable/cg_870f3f93/views/view_021_315deg.png",
"_turntable/cg_870f3f93/views/view_020_300deg.png",
"_turntable/cg_870f3f93/views/view_019_285deg.png",
"_turntable/cg_870f3f93/views/view_017_255deg.png",
"_turntable/cg_870f3f93/views/view_007_105deg.png",
"_turntable/cg_870f3f93/views/view_006_090deg.png",
"_turntable/cg_870f3f93/views/view_002_030deg.png",
"_turntable/cg_dc9c9bcc/views/view_023_345deg.png",
"_turntable/cg_dc9c9bcc/views/view_022_330deg.png",
"_turntable/cg_dc9c9bcc/views/view_021_315deg.png",
"_turntable/cg_dc9c9bcc/views/view_020_300deg.png",
"_turntable/cg_dc9c9bcc/views/view_019_285deg.png",
"_turntable/cg_dc9c9bcc/views/view_018_270deg.png",
"_turntable/cg_dc9c9bcc/views/view_017_255deg.png",
"_turntable/cg_dc9c9bcc/views/view_016_240deg.png",
"_turntable/cg_dc9c9bcc/views/view_015_225deg.png",
"_turntable/cg_dc9c9bcc/views/view_014_210deg.png",
"_turntable/cg_dc9c9bcc/views/view_013_195deg.png",
"_turntable/cg_dc9c9bcc/views/view_012_180deg.png",
"_turntable/cg_dc9c9bcc/views/view_011_165deg.png",
"_turntable/cg_dc9c9bcc/views/view_010_150deg.png",
"_turntable/cg_dc9c9bcc/views/view_009_135deg.png",
"_turntable/cg_dc9c9bcc/views/view_008_120deg.png",
"_turntable/cg_dc9c9bcc/views/view_007_105deg.png",
"_turntable/cg_dc9c9bcc/views/view_006_090deg.png",
"_turntable/cg_dc9c9bcc/views/view_005_075deg.png",
"_turntable/cg_dc9c9bcc/views/view_004_060deg.png",
"_turntable/cg_dc9c9bcc/views/view_003_045deg.png",
"_turntable/cg_dc9c9bcc/views/view_002_030deg.png",
"_turntable/cg_dc9c9bcc/views/view_001_015deg.png",
"_turntable/cg_dc9c9bcc/views/view_000_000deg.png",
"_turntable/20260618_134348_image.png/views/view_023_345deg.png",
"_turntable/20260618_134348_image.png/views/view_022_330deg.png",
"_turntable/20260618_134348_image.png/views/view_021_315deg.png",
"_turntable/20260618_134348_image.png/views/view_020_300deg.png",
"_turntable/20260618_134348_image.png/views/view_019_285deg.png",
"_turntable/20260618_134348_image.png/views/view_018_270deg.png",
"_turntable/20260618_134348_image.png/views/view_017_255deg.png",
"_turntable/20260618_134348_image.png/views/view_016_240deg.png",
"_turntable/20260618_134348_image.png/views/view_015_225deg.png",
"_turntable/20260618_134348_image.png/views/view_014_210deg.png",
"_turntable/20260618_134348_image.png/views/view_013_195deg.png",
"_turntable/20260618_134348_image.png/views/view_012_180deg.png",
"_turntable/20260618_134348_image.png/views/view_011_165deg.png",
"_turntable/20260618_134348_image.png/views/view_010_150deg.png",
"_turntable/20260618_134348_image.png/views/view_009_135deg.png",
"_turntable/20260618_134348_image.png/views/view_008_120deg.png",
"_turntable/20260618_134348_image.png/views/view_007_105deg.png",
"_turntable/20260618_134348_image.png/views/view_006_090deg.png",
"_turntable/20260618_134348_image.png/views/view_005_075deg.png",
"_turntable/20260618_134348_image.png/views/view_004_060deg.png",
"_turntable/20260618_134348_image.png/views/view_003_045deg.png",
"_turntable/20260618_134348_image.png/views/view_002_030deg.png",
"_turntable/20260618_134348_image.png/views/view_001_015deg.png",
"_turntable/20260618_134348_image.png/views/view_000_000deg.png",
"_turntable/20260618_125748_image.png/views/view_023_345deg.png",
"_turntable/20260618_125748_image.png/views/view_022_330deg.png",
"_turntable/20260618_125748_image.png/views/view_021_315deg.png",
"_turntable/20260618_125748_image.png/views/view_020_300deg.png",
"_turntable/20260618_125748_image.png/views/view_019_285deg.png",
"_turntable/20260618_125748_image.png/views/view_018_270deg.png",
"_turntable/20260618_125748_image.png/views/view_017_255deg.png",
"_turntable/20260618_125748_image.png/views/view_016_240deg.png",
"_turntable/20260618_125748_image.png/views/view_015_225deg.png",
"_turntable/20260618_125748_image.png/views/view_014_210deg.png",
"_turntable/20260618_125748_image.png/views/view_013_195deg.png",
"_turntable/20260618_125748_image.png/views/view_012_180deg.png",
"_turntable/20260618_125748_image.png/views/view_011_165deg.png",
"_turntable/20260618_125748_image.png/views/view_010_150deg.png",
"_turntable/20260618_125748_image.png/views/view_009_135deg.png",
"_turntable/20260618_125748_image.png/views/view_008_120deg.png",
"_turntable/20260618_125748_image.png/views/view_007_105deg.png",
"_turntable/20260618_125748_image.png/views/view_006_090deg.png",
"_turntable/20260618_125748_image.png/views/view_005_075deg.png",
"_turntable/20260618_125748_image.png/views/view_004_060deg.png",
"_turntable/20260618_125748_image.png/views/view_003_045deg.png",
"_turntable/20260618_125748_image.png/views/view_002_030deg.png",
"_turntable/20260618_125748_image.png/views/view_001_015deg.png",
"_turntable/20260618_125748_image.png/views/view_000_000deg.png",
"_turntable/cg_ac6274f0/views/view_023_345deg.png",
"_turntable/cg_ac6274f0/views/view_022_330deg.png",
"_turntable/cg_ac6274f0/views/view_021_315deg.png",
"_turntable/cg_ac6274f0/views/view_020_300deg.png",
"_turntable/cg_ac6274f0/views/view_019_285deg.png",
"_turntable/cg_ac6274f0/views/view_018_270deg.png",
"_turntable/cg_ac6274f0/views/view_017_255deg.png",
"_turntable/cg_ac6274f0/views/view_016_240deg.png",
"_turntable/cg_ac6274f0/views/view_015_225deg.png",
"_turntable/cg_ac6274f0/views/view_014_210deg.png",
"_turntable/cg_ac6274f0/views/view_013_195deg.png",
"_turntable/cg_ac6274f0/views/view_012_180deg.png",
"_turntable/cg_ac6274f0/views/view_011_165deg.png",
"_turntable/cg_ac6274f0/views/view_010_150deg.png",
"_turntable/cg_ac6274f0/views/view_009_135deg.png",
"_turntable/cg_ac6274f0/views/view_008_120deg.png",
"_turntable/cg_ac6274f0/views/view_007_105deg.png",
"_turntable/cg_ac6274f0/views/view_006_090deg.png",
"_turntable/cg_ac6274f0/views/view_005_075deg.png",
"_turntable/cg_ac6274f0/views/view_004_060deg.png",
"_turntable/cg_ac6274f0/views/view_003_045deg.png",
"_turntable/cg_ac6274f0/views/view_002_030deg.png",
"_turntable/cg_ac6274f0/views/view_001_015deg.png",
"_turntable/cg_ac6274f0/views/view_000_000deg.png",
"_turntable/20260618_181848_image.png/views/view_023_345deg.png",
"_turntable/20260618_181848_image.png/views/view_022_330deg.png",
"_turntable/20260618_181848_image.png/views/view_021_315deg.png",
"_turntable/20260618_181848_image.png/views/view_020_300deg.png",
"_turntable/20260618_181848_image.png/views/view_019_285deg.png",
"_turntable/20260618_181848_image.png/views/view_018_270deg.png",
"_turntable/20260618_181848_image.png/views/view_017_255deg.png",
"_turntable/20260618_181848_image.png/views/view_016_240deg.png",
"_turntable/20260618_181848_image.png/views/view_015_225deg.png",
"_turntable/20260618_181848_image.png/views/view_014_210deg.png",
"_turntable/20260618_181848_image.png/views/view_013_195deg.png",
"_turntable/20260618_181848_image.png/views/view_012_180deg.png",
"_turntable/20260618_181848_image.png/views/view_011_165deg.png",
"_turntable/20260618_181848_image.png/views/view_010_150deg.png",
"_turntable/20260618_181848_image.png/views/view_009_135deg.png",
"_turntable/20260618_181848_image.png/views/view_008_120deg.png",
"_turntable/20260618_181848_image.png/views/view_007_105deg.png",
"_turntable/20260618_181848_image.png/views/view_006_090deg.png",
"_turntable/20260618_181848_image.png/views/view_005_075deg.png",
"_turntable/20260618_181848_image.png/views/view_004_060deg.png",
"_turntable/20260618_181848_image.png/views/view_003_045deg.png",
"_turntable/20260618_181848_image.png/views/view_002_030deg.png",
"_turntable/20260618_181848_image.png/views/view_001_015deg.png",
"_turntable/20260618_181848_image.png/views/view_000_000deg.png",
"_turntable/cg_65f71c4d/views/view_022_330deg.png",
"_turntable/cg_65f71c4d/views/view_021_315deg.png",
"_turntable/cg_65f71c4d/views/view_020_300deg.png",
"_turntable/cg_65f71c4d/views/view_019_285deg.png",
"_turntable/cg_65f71c4d/views/view_018_270deg.png",
"_turntable/cg_65f71c4d/views/view_017_255deg.png",
"_turntable/cg_65f71c4d/views/view_016_240deg.png",
"_turntable/cg_65f71c4d/views/view_015_225deg.png",
"_turntable/cg_65f71c4d/views/view_014_210deg.png",
"_turntable/cg_65f71c4d/views/view_013_195deg.png",
"_turntable/cg_65f71c4d/views/view_012_180deg.png",
"_turntable/cg_65f71c4d/views/view_011_165deg.png",
"_turntable/cg_65f71c4d/views/view_010_150deg.png",
"_turntable/cg_65f71c4d/views/view_009_135deg.png",
"_turntable/cg_65f71c4d/views/view_008_120deg.png",
"_turntable/cg_65f71c4d/views/view_007_105deg.png",
"_turntable/cg_65f71c4d/views/view_006_090deg.png",
"_turntable/cg_65f71c4d/views/view_005_075deg.png",
"_turntable/cg_65f71c4d/views/view_004_060deg.png",
"_turntable/cg_65f71c4d/views/view_003_045deg.png",
"_turntable/cg_65f71c4d/views/view_002_030deg.png",
"_turntable/cg_ed2e43d1/views/view_021_315deg.png",
"_turntable/cg_ed2e43d1/views/view_020_300deg.png",
"_turntable/cg_ed2e43d1/views/view_019_285deg.png",
"_turntable/cg_ed2e43d1/views/view_006_090deg.png",
"_turntable/cg_ed2e43d1/views/view_005_075deg.png",
"_turntable/cg_ed2e43d1/views/view_004_060deg.png",
"_turntable/cg_ed2e43d1/views/view_003_045deg.png",
"_turntable/20260618_125217_image.png/views/view_023_345deg.png",
"_turntable/20260618_125217_image.png/views/view_022_330deg.png",
"_turntable/20260618_125217_image.png/views/view_021_315deg.png",
"_turntable/20260618_125217_image.png/views/view_020_300deg.png",
"_turntable/20260618_125217_image.png/views/view_019_285deg.png",
"_turntable/20260618_125217_image.png/views/view_018_270deg.png",
"_turntable/20260618_125217_image.png/views/view_017_255deg.png",
"_turntable/20260618_125217_image.png/views/view_016_240deg.png",
"_turntable/20260618_125217_image.png/views/view_015_225deg.png",
"_turntable/20260618_125217_image.png/views/view_014_210deg.png",
"_turntable/20260618_125217_image.png/views/view_013_195deg.png",
"_turntable/20260618_125217_image.png/views/view_012_180deg.png",
"_turntable/20260618_125217_image.png/views/view_011_165deg.png",
"_turntable/20260618_125217_image.png/views/view_010_150deg.png",
"_turntable/20260618_125217_image.png/views/view_009_135deg.png",
"_turntable/20260618_125217_image.png/views/view_008_120deg.png",
"_turntable/20260618_125217_image.png/views/view_007_105deg.png",
"_turntable/20260618_125217_image.png/views/view_006_090deg.png",
"_turntable/20260618_125217_image.png/views/view_005_075deg.png",
"_turntable/20260618_125217_image.png/views/view_004_060deg.png",
"_turntable/20260618_125217_image.png/views/view_003_045deg.png",
"_turntable/20260618_125217_image.png/views/view_002_030deg.png",
"_turntable/20260618_125217_image.png/views/view_001_015deg.png",
"_turntable/20260618_125217_image.png/views/view_000_000deg.png",
"_turntable/up_9f6b53b9/views/view_023_345deg.png",
"_turntable/up_9f6b53b9/views/view_022_330deg.png",
"_turntable/up_9f6b53b9/views/view_021_315deg.png",
"_turntable/up_9f6b53b9/views/view_020_300deg.png",
"_turntable/up_9f6b53b9/views/view_019_285deg.png",
"_turntable/up_9f6b53b9/views/view_018_270deg.png",
"_turntable/up_9f6b53b9/views/view_017_255deg.png",
"_turntable/up_9f6b53b9/views/view_016_240deg.png",
"_turntable/up_9f6b53b9/views/view_015_225deg.png",
"_turntable/up_9f6b53b9/views/view_014_210deg.png",
"_turntable/up_9f6b53b9/views/view_013_195deg.png",
"_turntable/up_9f6b53b9/views/view_012_180deg.png",
"_turntable/up_9f6b53b9/views/view_011_165deg.png",
"_turntable/up_9f6b53b9/views/view_010_150deg.png",
"_turntable/up_9f6b53b9/views/view_009_135deg.png",
"_turntable/up_9f6b53b9/views/view_008_120deg.png",
"_turntable/up_9f6b53b9/views/view_007_105deg.png",
"_turntable/up_9f6b53b9/views/view_006_090deg.png",
"_turntable/up_9f6b53b9/views/view_005_075deg.png",
"_turntable/up_9f6b53b9/views/view_004_060deg.png",
"_turntable/up_9f6b53b9/views/view_003_045deg.png",
"_turntable/up_9f6b53b9/views/view_002_030deg.png",
"_turntable/up_9f6b53b9/views/view_001_015deg.png",
"_turntable/up_9f6b53b9/views/view_000_000deg.png",
"_turntable/cg_f9537c3f/views/view_022_330deg.png",
"_turntable/cg_f9537c3f/views/view_021_315deg.png",
"_turntable/cg_f9537c3f/views/view_020_300deg.png",
"_turntable/cg_f9537c3f/views/view_019_285deg.png",
"_turntable/cg_f9537c3f/views/view_018_270deg.png",
"_turntable/cg_f9537c3f/views/view_017_255deg.png",
"_turntable/cg_f9537c3f/views/view_016_240deg.png",
"_turntable/cg_f9537c3f/views/view_015_225deg.png",
"_turntable/cg_f9537c3f/views/view_014_210deg.png",
"_turntable/cg_f9537c3f/views/view_013_195deg.png",
"_turntable/cg_f9537c3f/views/view_012_180deg.png",
"_turntable/cg_f9537c3f/views/view_011_165deg.png",
"_turntable/cg_f9537c3f/views/view_010_150deg.png",
"_turntable/cg_f9537c3f/views/view_009_135deg.png",
"_turntable/cg_f9537c3f/views/view_008_120deg.png",
"_turntable/cg_f9537c3f/views/view_007_105deg.png",
"_turntable/cg_f9537c3f/views/view_006_090deg.png",
"_turntable/cg_f9537c3f/views/view_005_075deg.png",
"_turntable/cg_f9537c3f/views/view_004_060deg.png",
"_turntable/cg_f9537c3f/views/view_003_045deg.png",
"_turntable/cg_f9537c3f/views/view_002_030deg.png",
"_turntable/cg_58c0891c/views/view_023_345deg.png",
"_turntable/cg_58c0891c/views/view_022_330deg.png",
"_turntable/cg_58c0891c/views/view_021_315deg.png",
"_turntable/cg_58c0891c/views/view_020_300deg.png",
"_turntable/cg_58c0891c/views/view_019_285deg.png",
"_turntable/cg_58c0891c/views/view_018_270deg.png",
"_turntable/cg_58c0891c/views/view_017_255deg.png",
"_turntable/cg_58c0891c/views/view_016_240deg.png",
"_turntable/cg_58c0891c/views/view_015_225deg.png",
"_turntable/cg_58c0891c/views/view_014_210deg.png",
"_turntable/cg_58c0891c/views/view_013_195deg.png",
"_turntable/cg_58c0891c/views/view_012_180deg.png",
"_turntable/cg_58c0891c/views/view_011_165deg.png",
"_turntable/cg_58c0891c/views/view_010_150deg.png",
"_turntable/cg_58c0891c/views/view_009_135deg.png",
"_turntable/cg_58c0891c/views/view_008_120deg.png",
"_turntable/cg_58c0891c/views/view_007_105deg.png",
"_turntable/cg_58c0891c/views/view_006_090deg.png",
"_turntable/cg_58c0891c/views/view_005_075deg.png",
"_turntable/cg_58c0891c/views/view_004_060deg.png",
"_turntable/cg_58c0891c/views/view_003_045deg.png",
"_turntable/cg_58c0891c/views/view_002_030deg.png",
"_turntable/cg_58c0891c/views/view_001_015deg.png",
"_turntable/cg_58c0891c/views/view_000_000deg.png",
"_turntable/cg_5991f3f2/views/view_023_345deg.png",
"_turntable/cg_5991f3f2/views/view_021_315deg.png",
"_turntable/cg_5991f3f2/views/view_020_300deg.png",
"_turntable/cg_5991f3f2/views/view_019_285deg.png",
"_turntable/cg_5991f3f2/views/view_018_270deg.png",
"_turntable/cg_5991f3f2/views/view_017_255deg.png",
"_turntable/cg_5991f3f2/views/view_016_240deg.png",
"_turntable/cg_5991f3f2/views/view_015_225deg.png",
"_turntable/cg_5991f3f2/views/view_014_210deg.png",
"_turntable/cg_5991f3f2/views/view_013_195deg.png",
"_turntable/cg_5991f3f2/views/view_012_180deg.png",
"_turntable/cg_5991f3f2/views/view_011_165deg.png",
"_turntable/cg_5991f3f2/views/view_010_150deg.png",
"_turntable/cg_5991f3f2/views/view_009_135deg.png",
"_turntable/cg_5991f3f2/views/view_008_120deg.png",
"_turntable/cg_5991f3f2/views/view_007_105deg.png",
"_turntable/cg_5991f3f2/views/view_006_090deg.png",
"_turntable/cg_5991f3f2/views/view_005_075deg.png",
"_turntable/cg_5991f3f2/views/view_004_060deg.png",
"_turntable/cg_5991f3f2/views/view_003_045deg.png",
"_turntable/cg_5991f3f2/views/view_001_015deg.png",
"_turntable/cg_5991f3f2/views/view_000_000deg.png",
"_turntable/out4.png/views/view_023_345deg.png",
"_turntable/out4.png/views/view_022_330deg.png",
"_turntable/out4.png/views/view_021_315deg.png",
"_turntable/out4.png/views/view_020_300deg.png",
"_turntable/out4.png/views/view_019_285deg.png",
"_turntable/out4.png/views/view_018_270deg.png",
"_turntable/out4.png/views/view_017_255deg.png",
"_turntable/out4.png/views/view_016_240deg.png",
"_turntable/out4.png/views/view_015_225deg.png",
"_turntable/out4.png/views/view_014_210deg.png",
"_turntable/out4.png/views/view_013_195deg.png",
"_turntable/out4.png/views/view_012_180deg.png",
"_turntable/out4.png/views/view_011_165deg.png",
"_turntable/out4.png/views/view_010_150deg.png",
"_turntable/out4.png/views/view_009_135deg.png",
"_turntable/cg_18040099/views/view_023_345deg.png",
"_turntable/cg_18040099/views/view_022_330deg.png",
"_turntable/cg_18040099/views/view_021_315deg.png",
"_turntable/cg_18040099/views/view_020_300deg.png",
"_turntable/cg_18040099/views/view_019_285deg.png",
"_turntable/cg_18040099/views/view_018_270deg.png",
"_turntable/cg_18040099/views/view_017_255deg.png",
"_turntable/cg_18040099/views/view_016_240deg.png",
"_turntable/cg_18040099/views/view_015_225deg.png",
"_turntable/cg_18040099/views/view_014_210deg.png",
"_turntable/cg_18040099/views/view_013_195deg.png",
"_turntable/cg_18040099/views/view_012_180deg.png",
"_turntable/cg_18040099/views/view_011_165deg.png",
"_turntable/cg_18040099/views/view_010_150deg.png",
"_turntable/cg_18040099/views/view_009_135deg.png",
"_turntable/cg_18040099/views/view_008_120deg.png",
"_turntable/cg_18040099/views/view_007_105deg.png",
"_turntable/cg_18040099/views/view_006_090deg.png",
"_turntable/cg_18040099/views/view_005_075deg.png",
"_turntable/cg_18040099/views/view_004_060deg.png",
"_turntable/cg_18040099/views/view_003_045deg.png",
"_turntable/cg_18040099/views/view_002_030deg.png",
"_turntable/cg_18040099/views/view_001_015deg.png",
"_turntable/cg_18040099/views/view_000_000deg.png",
"20260627_044054_pad_20260621_205908_0_20260621_205151_image.png",
"20260627_044022_pad_20260621_205908_0_20260621_205151_image.png",
"20260627_043800_dup_20260624_055327_image.nobg.nobg.png",
"20260624_125511_dup_20260624_055327_image.nobg.nobg.png",
"20260624_125511_dup_20260624_055327_image.nobg.nobg.nobg.png",
"20260627_043555_dup_20260624_055327_image.nobg.nobg.nobg.png",
"20260627_043555_dup_20260624_055327_image.nobg.nobg.png",
"20260627_043455_dup_20260624_055327_image.nobg.png",
"20260624_125511_dup_20260624_055327_image.nobg.png",
"20260627_043155_crop_20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
"20260627_043055_pad_20260627_042228_image.nobg.png",
"20260627_042900_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
"20260627_042835_crop_20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
"20260627_042719_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
"20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
"20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.nobg.png",
"20260627_042620_pad_20260627_042228_image.nobg.png",
"20260627_042554_pad_20260627_042228_image.nobg.png",
"20260627_042554_pad_20260627_042228_image.nobg.nobg.png",
"20260627_042525_image.nobg.png",
"20260627_042228_image.nobg.png",
"20260627_042228_image.png",
"20260627_042256_image.png",
"20260627_042100_pose_image.png",
"20260627_042003_image.png",
"20260627_041915_image.png",
"20260627_041701_image.png",
"20260627_041236_image.nobg.png",
"20260627_041236_image.png",
"20260621_033259_image.nobg.png",
"20260627_040944_pad_20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_040926_pad_20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_040817_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_040738_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_040304_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_040022_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_035757_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_035559_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_035145_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.nobg.png",
"20260627_035026_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.nobg.png",
"20260627_034934_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.png",
"20260627_034736_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_034657_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_034539_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_034516_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_034136_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033930_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033810_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033515_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033439_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033320_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033211_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033128_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_033036_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_032921_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_031155_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_031104_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_030811_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_030739_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"cg_18040099_face.png",
"20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_030441_crop_20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
"20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.png",
"20260627_030344_sc_image.png",
"20260627_030145_sc_dup_20260624_055327_image.nobg.png",
"20260627_030108_sc_dup_20260624_055327_image.nobg.png",
"20260627_030017_sc_dup_20260624_055327_image.nobg.png",
"20260627_025856_image.png",
"20260627_025639_sc_2_20260619_202924_image.png",
"20260627_025354_sc_2_20260619_202924_image.png",
"20260627_025021_sc_image.png",
"20260627_024950_sc_image.png",
"20260627_024719_sc_image.png",
"20260627_024645_image.png",
"20260627_024603_sc_image.png",
"20260627_024532_sc_sc_image.png",
"20260627_024445_image.png",
"20260627_024048_sc_image.nobg.png",
"20260627_023522_sc_image.nobg.png",
"20260627_023305_image.png",
"20260627_022851_image.nobg.png",
"20260627_022636_image.nobg.png",
"20260627_022513_image.nobg.png",
"20260627_022032_image.nobg.png",
"20260627_021300_sc_image.png",
"20260627_021145_image.nobg.png",
"20260627_020858_sc_image.png",
"20260627_020758_sc_image.png",
"20260627_020659_sc_image.png",
"20260627_020432_image.nobg.png",
"20260627_020352_image.png",
"20260627_020333_image.png",
"20260627_020210_image.png",
"20260627_015719_sc_image.png",
"20260627_014938_sc_image.png",
"20260627_014744_sc_image.png",
"20260627_014710_sc_image.png",
"20260627_013752_sc_image.png",
"20260627_013633_sc_image.png",
"20260627_013548_sc_image.png",
"cg_37d910ad_face.png",
"20260627_013006_sc_image.png",
"20260627_012859_sc_image.png",
"20260627_012814_image.png",
"_turntable/20260618_181600_image.png/views/view_011_330deg.png",
"_turntable/20260618_181600_image.png/views/view_010_300deg.png",
"_turntable/20260618_181600_image.png/views/view_009_270deg.png",
"_turntable/20260618_181600_image.png/views/view_008_240deg.png",
"_turntable/20260618_181600_image.png/views/view_007_210deg.png",
"_turntable/20260618_181600_image.png/views/view_006_180deg.png",
"_turntable/20260618_181600_image.png/views/view_005_150deg.png",
"_turntable/20260618_181600_image.png/views/view_004_120deg.png",
"_turntable/20260618_181600_image.png/views/view_003_090deg.png",
"_turntable/20260618_181600_image.png/views/view_002_060deg.png",
"_turntable/20260618_181600_image.png/views/view_001_030deg.png",
"_turntable/20260618_181600_image.png/views/view_000_000deg.png",
"20260626_234431_crop_20260626_231655_sc_image.nobg.png",
"20260626_231655_sc_image.nobg.png",
"20260618_181600_image.png_face.png",
"20260626_231655_sc_image.png",
"20260626_234334_dup_20260626_231655_sc_image.png",
"20260626_231349_sc_image.png",
"20260626_231127_image.png",
"_turntable/up_624995c3/views/view_011_330deg.png",
"_turntable/up_624995c3/views/view_010_300deg.png",
"_turntable/up_624995c3/views/view_009_270deg.png",
"_turntable/up_624995c3/views/view_008_240deg.png",
"_turntable/up_624995c3/views/view_007_210deg.png",
"_turntable/up_624995c3/views/view_006_180deg.png",
"_turntable/up_624995c3/views/view_005_150deg.png",
"_turntable/up_624995c3/views/view_004_120deg.png",
"_turntable/up_624995c3/views/view_003_090deg.png",
"_turntable/up_624995c3/views/view_002_060deg.png",
"_turntable/up_624995c3/views/view_001_030deg.png",
"_turntable/up_624995c3/views/view_000_000deg.png",
"up_624995c3_face.png",
"20260626_224632_sc_image.png",
"20260626_224037_sc_image.png",
"20260626_223814_sc_image.png",
"20260626_222911_sc_image.png",
"20260626_222706_sc_image.png",
"20260626_222509_sc_image.png",
"20260626_222302_sc_image.png",
"_turntable/up_624995c3/views/view_002_030deg.png",
"_turntable/up_624995c3/views/view_001_015deg.png",
"20260626_220958_image.png",
"_turntable/cg_37d910ad/views/view_011_330deg.png",
"_turntable/cg_37d910ad/views/view_010_300deg.png",
"_turntable/cg_37d910ad/views/view_009_270deg.png",
"_turntable/cg_37d910ad/views/view_008_240deg.png",
"_turntable/cg_37d910ad/views/view_007_210deg.png",
"_turntable/cg_37d910ad/views/view_006_180deg.png",
"_turntable/cg_37d910ad/views/view_005_150deg.png",
"_turntable/cg_37d910ad/views/view_004_120deg.png",
"_turntable/cg_37d910ad/views/view_003_090deg.png",
"_turntable/cg_37d910ad/views/view_002_060deg.png",
"_turntable/cg_37d910ad/views/view_001_030deg.png",
"_turntable/cg_37d910ad/views/view_000_000deg.png",
"_turntable/out4.png/views/view_007_105deg.png",
"_turntable/out4.png/views/view_006_090deg.png",
"_turntable/out4.png/views/view_005_075deg.png",
"_turntable/out4.png/views/view_004_060deg.png",
"_turntable/out4.png/views/view_003_045deg.png",
"_turntable/out4.png/views/view_002_030deg.png",
"_turntable/out4.png/views/view_001_015deg.png",
"_turntable/out4.png/views/view_000_000deg.png",
"_turntable/cg_d032c1ef/views/view_023_345deg.png",
"_turntable/cg_d032c1ef/views/view_022_330deg.png",
"_turntable/cg_d032c1ef/views/view_021_315deg.png",
"_turntable/cg_d032c1ef/views/view_020_300deg.png",
"_turntable/cg_d032c1ef/views/view_019_285deg.png",
"_turntable/cg_d032c1ef/views/view_018_270deg.png",
"_turntable/cg_d032c1ef/views/view_017_255deg.png",
"_turntable/cg_d032c1ef/views/view_016_240deg.png",
"_turntable/cg_d032c1ef/views/view_015_225deg.png",
"_turntable/cg_d032c1ef/views/view_014_210deg.png",
"_turntable/cg_d032c1ef/views/view_013_195deg.png",
"_turntable/cg_d032c1ef/views/view_012_180deg.png",
"_turntable/cg_d032c1ef/views/view_011_165deg.png",
"_turntable/cg_d032c1ef/views/view_010_150deg.png",
"_turntable/cg_d032c1ef/views/view_009_135deg.png",
"_turntable/cg_d032c1ef/views/view_008_120deg.png",
"_turntable/cg_d032c1ef/views/view_007_105deg.png",
"_turntable/cg_d032c1ef/views/view_006_090deg.png",
"_turntable/cg_d032c1ef/views/view_005_075deg.png",
"_turntable/cg_d032c1ef/views/view_004_060deg.png",
"_turntable/cg_d032c1ef/views/view_003_045deg.png",
"_turntable/cg_d032c1ef/views/view_002_030deg.png",
"_turntable/cg_d032c1ef/views/view_001_015deg.png",
"_turntable/cg_d032c1ef/views/view_000_000deg.png",
"_turntable/20260618_174516_image.png/views/view_023_345deg.png",
"_turntable/20260618_174516_image.png/views/view_022_330deg.png",
"_turntable/20260618_174516_image.png/views/view_021_315deg.png",
"_turntable/20260618_174516_image.png/views/view_020_300deg.png",
"_turntable/20260618_174516_image.png/views/view_019_285deg.png",
"_turntable/20260618_174516_image.png/views/view_018_270deg.png",
"_turntable/20260618_174516_image.png/views/view_017_255deg.png",
"_turntable/20260618_174516_image.png/views/view_016_240deg.png",
"_turntable/20260618_174516_image.png/views/view_015_225deg.png",
"_turntable/20260618_174516_image.png/views/view_014_210deg.png",
"_turntable/20260618_174516_image.png/views/view_013_195deg.png",
"_turntable/20260618_174516_image.png/views/view_012_180deg.png",
"_turntable/20260618_174516_image.png/views/view_011_165deg.png",
"_turntable/20260618_174516_image.png/views/view_010_150deg.png",
"_turntable/20260618_174516_image.png/views/view_009_135deg.png",
"_turntable/20260618_174516_image.png/views/view_008_120deg.png",
"_turntable/20260618_174516_image.png/views/view_007_105deg.png",
"_turntable/20260618_174516_image.png/views/view_006_090deg.png",
"_turntable/20260618_174516_image.png/views/view_005_075deg.png",
"_turntable/20260618_174516_image.png/views/view_004_060deg.png",
"_turntable/20260618_174516_image.png/views/view_003_045deg.png",
"_turntable/20260618_174516_image.png/views/view_002_030deg.png",
"_turntable/20260618_174516_image.png/views/view_001_015deg.png",
"_turntable/20260618_174516_image.png/views/view_000_000deg.png",
"_turntable/20260618_054230_image.png/views/view_023_345deg.png",
"_turntable/20260618_054230_image.png/views/view_022_330deg.png",
"_turntable/20260618_054230_image.png/views/view_021_315deg.png",
"_turntable/20260618_054230_image.png/views/view_020_300deg.png",
"_turntable/20260618_054230_image.png/views/view_019_285deg.png",
"_turntable/20260618_054230_image.png/views/view_018_270deg.png",
"_turntable/20260618_054230_image.png/views/view_017_255deg.png",
"_turntable/20260618_054230_image.png/views/view_016_240deg.png",
"_turntable/20260618_054230_image.png/views/view_015_225deg.png",
"_turntable/20260618_054230_image.png/views/view_014_210deg.png",
"_turntable/20260618_054230_image.png/views/view_013_195deg.png",
"_turntable/20260618_054230_image.png/views/view_012_180deg.png",
"_turntable/20260618_054230_image.png/views/view_011_165deg.png",
"_turntable/20260618_054230_image.png/views/view_010_150deg.png",
"_turntable/20260618_054230_image.png/views/view_009_135deg.png",
"_turntable/20260618_054230_image.png/views/view_008_120deg.png",
"_turntable/20260618_054230_image.png/views/view_007_105deg.png",
"_turntable/20260618_054230_image.png/views/view_006_090deg.png",
"_turntable/20260618_054230_image.png/views/view_005_075deg.png",
"_turntable/20260618_054230_image.png/views/view_004_060deg.png",
"_turntable/20260618_054230_image.png/views/view_003_045deg.png",
"_turntable/20260618_054230_image.png/views/view_002_030deg.png",
"_turntable/20260618_054230_image.png/views/view_001_015deg.png",
"_turntable/20260618_054230_image.png/views/view_000_000deg.png",
"_turntable/cg_f1e85987/views/view_023_345deg.png",
"_turntable/cg_f1e85987/views/view_022_330deg.png",
"_turntable/cg_f1e85987/views/view_021_315deg.png",
"_turntable/cg_f1e85987/views/view_020_300deg.png",
"_turntable/cg_f1e85987/views/view_019_285deg.png",
"_turntable/cg_f1e85987/views/view_018_270deg.png",
"_turntable/cg_f1e85987/views/view_017_255deg.png",
"_turntable/cg_f1e85987/views/view_016_240deg.png",
"_turntable/cg_f1e85987/views/view_015_225deg.png",
"_turntable/cg_f1e85987/views/view_014_210deg.png",
"_turntable/cg_f1e85987/views/view_013_195deg.png",
"_turntable/cg_f1e85987/views/view_012_180deg.png",
"_turntable/cg_f1e85987/views/view_011_165deg.png",
"_turntable/cg_f1e85987/views/view_010_150deg.png",
"_turntable/cg_f1e85987/views/view_009_135deg.png",
"_turntable/cg_f1e85987/views/view_008_120deg.png",
"_turntable/cg_f1e85987/views/view_007_105deg.png",
"_turntable/cg_f1e85987/views/view_006_090deg.png",
"_turntable/cg_f1e85987/views/view_005_075deg.png",
"_turntable/cg_f1e85987/views/view_004_060deg.png",
"_turntable/cg_f1e85987/views/view_003_045deg.png",
"_turntable/cg_f1e85987/views/view_002_030deg.png",
"_turntable/cg_f1e85987/views/view_001_015deg.png",
"_turntable/cg_f1e85987/views/view_000_000deg.png",
"_turntable/up_76ef7e4f/views/view_023_345deg.png",
"_turntable/up_76ef7e4f/views/view_022_330deg.png",
"_turntable/up_76ef7e4f/views/view_021_315deg.png",
"_turntable/up_76ef7e4f/views/view_020_300deg.png",
"_turntable/up_76ef7e4f/views/view_019_285deg.png",
"_turntable/up_76ef7e4f/views/view_018_270deg.png",
"_turntable/up_76ef7e4f/views/view_017_255deg.png",
"_turntable/up_76ef7e4f/views/view_016_240deg.png",
"_turntable/up_76ef7e4f/views/view_015_225deg.png",
"_turntable/up_76ef7e4f/views/view_014_210deg.png",
"_turntable/up_76ef7e4f/views/view_013_195deg.png",
"_turntable/up_76ef7e4f/views/view_012_180deg.png",
"_turntable/up_76ef7e4f/views/view_011_165deg.png",
"_turntable/up_76ef7e4f/views/view_010_150deg.png",
"_turntable/up_76ef7e4f/views/view_009_135deg.png",
"_turntable/up_76ef7e4f/views/view_008_120deg.png",
"_turntable/up_76ef7e4f/views/view_007_105deg.png",
"_turntable/up_76ef7e4f/views/view_006_090deg.png",
"_turntable/up_76ef7e4f/views/view_005_075deg.png",
"_turntable/up_76ef7e4f/views/view_004_060deg.png",
"_turntable/up_76ef7e4f/views/view_003_045deg.png",
"_turntable/up_76ef7e4f/views/view_002_030deg.png",
"_turntable/up_76ef7e4f/views/view_001_015deg.png",
"_turntable/up_76ef7e4f/views/view_000_000deg.png",
"_turntable/up_a4d16da7/turntable.mp4",
"_turntable/up_a4d16da7/views/view_023_345deg.png",
"_turntable/up_a4d16da7/views/view_022_330deg.png",
"_turntable/up_a4d16da7/views/view_021_315deg.png",
"_turntable/up_a4d16da7/views/view_020_300deg.png",
"_turntable/up_a4d16da7/views/view_019_285deg.png",
"_turntable/up_a4d16da7/views/view_018_270deg.png",
"_turntable/up_a4d16da7/views/view_017_255deg.png",
"_turntable/up_a4d16da7/views/view_016_240deg.png",
"_turntable/up_a4d16da7/views/view_015_225deg.png",
"_turntable/up_a4d16da7/views/view_014_210deg.png",
"_turntable/up_a4d16da7/views/view_013_195deg.png",
"_turntable/up_a4d16da7/views/view_012_180deg.png",
"_turntable/up_a4d16da7/views/view_011_165deg.png",
"_turntable/up_a4d16da7/views/view_010_150deg.png",
"_turntable/up_a4d16da7/views/view_009_135deg.png",
"_turntable/up_a4d16da7/views/view_008_120deg.png",
"_turntable/up_a4d16da7/views/view_007_105deg.png",
"_turntable/up_a4d16da7/views/view_006_090deg.png",
"_turntable/up_a4d16da7/views/view_005_075deg.png",
"_turntable/up_a4d16da7/views/view_004_060deg.png",
"_turntable/up_a4d16da7/views/view_003_045deg.png",
"_turntable/up_a4d16da7/views/view_002_030deg.png",
"_turntable/up_a4d16da7/views/view_001_015deg.png",
"_turntable/up_a4d16da7/views/view_000_000deg.png",
"_turntable/20260618_180939_image.png/views/view_023_345deg.png",
"_turntable/20260618_180939_image.png/views/view_022_330deg.png",
"_turntable/20260618_180939_image.png/views/view_021_315deg.png",
"_turntable/20260618_180939_image.png/views/view_020_300deg.png",
"_turntable/20260618_180939_image.png/views/view_019_285deg.png",
"_turntable/20260618_180939_image.png/views/view_018_270deg.png",
"_turntable/20260618_180939_image.png/views/view_017_255deg.png",
"_turntable/20260618_180939_image.png/views/view_016_240deg.png",
"_turntable/20260618_180939_image.png/views/view_015_225deg.png",
"_turntable/20260618_180939_image.png/views/view_014_210deg.png",
"_turntable/20260618_180939_image.png/views/view_013_195deg.png",
"_turntable/20260618_180939_image.png/views/view_012_180deg.png",
"_turntable/20260618_180939_image.png/views/view_011_165deg.png",
"_turntable/20260618_180939_image.png/views/view_010_150deg.png",
"_turntable/20260618_180939_image.png/views/view_009_135deg.png",
"_turntable/20260618_180939_image.png/views/view_008_120deg.png",
"_turntable/20260618_180939_image.png/views/view_007_105deg.png",
"_turntable/20260618_180939_image.png/views/view_006_090deg.png",
"_turntable/20260618_180939_image.png/views/view_005_075deg.png",
"_turntable/20260618_180939_image.png/views/view_004_060deg.png",
"_turntable/20260618_180939_image.png/views/view_003_045deg.png",
"_turntable/20260618_180939_image.png/views/view_002_030deg.png",
"_turntable/20260618_180939_image.png/views/view_001_015deg.png",
"_turntable/20260618_180939_image.png/views/view_000_000deg.png",
"_turntable/cg_2afca8fc/views/view_023_345deg.png",
"_turntable/cg_2afca8fc/views/view_022_330deg.png",
"_turntable/cg_2afca8fc/views/view_021_315deg.png",
"_turntable/cg_2afca8fc/views/view_020_300deg.png",
"_turntable/cg_2afca8fc/views/view_019_285deg.png",
"_turntable/cg_2afca8fc/views/view_018_270deg.png",
"_turntable/cg_2afca8fc/views/view_017_255deg.png",
"_turntable/cg_2afca8fc/views/view_015_225deg.png",
"_turntable/cg_2afca8fc/views/view_014_210deg.png",
"_turntable/cg_2afca8fc/views/view_012_180deg.png",
"_turntable/cg_2afca8fc/views/view_011_165deg.png",
"_turntable/cg_2afca8fc/views/view_010_150deg.png",
"_turntable/cg_2afca8fc/views/view_007_105deg.png",
"_turntable/cg_2afca8fc/views/view_006_090deg.png",
"_turntable/cg_2afca8fc/views/view_005_075deg.png",
"_turntable/cg_2afca8fc/views/view_004_060deg.png",
"_turntable/cg_2afca8fc/views/view_003_045deg.png",
"_turntable/cg_2afca8fc/views/view_002_030deg.png",
"_turntable/cg_2afca8fc/views/view_000_000deg.png",
"_turntable/cg_7c7b22c9/views/view_023_345deg.png",
"_turntable/cg_7c7b22c9/views/view_021_315deg.png",
"_turntable/cg_7c7b22c9/views/view_020_300deg.png",
"_turntable/cg_7c7b22c9/views/view_019_285deg.png",
"_turntable/cg_7c7b22c9/views/view_018_270deg.png",
"_turntable/cg_7c7b22c9/views/view_017_255deg.png",
"_turntable/cg_7c7b22c9/views/view_016_240deg.png",
"_turntable/cg_7c7b22c9/views/view_015_225deg.png",
"_turntable/cg_7c7b22c9/views/view_014_210deg.png",
"_turntable/cg_7c7b22c9/views/view_013_195deg.png",
"_turntable/cg_7c7b22c9/views/view_012_180deg.png",
"_turntable/cg_7c7b22c9/views/view_011_165deg.png",
"_turntable/cg_7c7b22c9/views/view_010_150deg.png",
"up_b7db33a9_face.png",
"20260626_162432_6_20260626_161654_image.png",
"20260626_162410_image.png",
"20260626_161941_image.png",
"20260626_161844_6_20260626_161654_image.png",
"20260626_161727_0_20260626_161654_image.png",
"_turntable/cg_7c7b22c9/views/view_009_135deg.png",
"20260626_161654_image.png",
"_turntable/cg_7c7b22c9/views/view_008_120deg.png",
"_turntable/cg_7c7b22c9/views/view_007_105deg.png",
"_turntable/cg_7c7b22c9/views/view_006_090deg.png",
"_turntable/cg_7c7b22c9/views/view_005_075deg.png",
"_turntable/cg_7c7b22c9/views/view_004_060deg.png",
"_turntable/cg_7c7b22c9/views/view_003_045deg.png",
"_turntable/cg_7c7b22c9/views/view_002_030deg.png",
"_turntable/cg_7c7b22c9/views/view_001_015deg.png",
"_turntable/cg_7c7b22c9/views/view_000_000deg.png",
"cg_e79533a4_face.png",
"cg_73e141fa_face.png",
"20260626_154945_image.png",
"20260626_154934_image.png",
"20260626_154922_image.png",
"20260626_154745_image.png",
"20260626_154728_mr_image.png",
"20260626_154605_image.png",
"20260626_154535_mr_image.png",
"20260626_154301_mr_image.png",
"20260626_153213_image.png",
"20260626_153157_image.png",
"20260626_153133_image.png",
"20260618_015041_20260618_004313_20260616_021938_20160903_200935.jpg",
"20260618_015041_20260618_004313_20260616_021938_20160903_200935.nobg.nobg.png",
"20260626_124145_20160903_200935.jpg",
"cg_4671b6c4_face.png",
"20260626_152247_crop_20260626_152107_20160903_200935.nobg.nobg.nobg.nobg.png",
"20260626_152107_20160903_200935.nobg.png",
"20260626_151945_dup_20260626_151844_20160903_200935.nobg.png",
"20260626_151844_20160903_200935.nobg.png",
"20260626_145831_9_20260626_145421_image.png",
"20260626_145818_8_20260626_145421_image.png",
"20260626_145803_7_20260626_145421_image.png",
"_turntable/up_5b23ff92/views/view_016_240deg.png",
"20260626_145717_6_20260626_145421_image.png",
"_turntable/up_5b23ff92/views/view_015_225deg.png",
"20260626_145636_5_20260626_145421_image.png",
"20260626_145620_4_20260626_145421_image.png",
"20260626_145604_3_20260626_145421_image.png",
"20260626_145547_2_20260626_145421_image.png",
"20260626_145531_1_20260626_145421_image.png",
"20260626_145514_0_20260626_145421_image.png",
"20260626_145421_image.png",
"20260626_145037_image.png",
"20260626_144719_9_20260626_144519_image.png",
"20260626_144656_7_20260626_144519_image.png",
"20260626_144645_6_20260626_144519_image.png",
"20260626_144611_3_20260626_144519_image.png",
"20260626_144600_2_20260626_144519_image.png",
"20260626_144549_1_20260626_144519_image.png",
"20260626_144538_0_20260626_144519_image.png",
"20260626_144519_image.png",
"_turntable/up_5b23ff92/views/view_014_210deg.png",
"20260626_144226_image.png",
"up_819bf34a_face.png",
"20260624_001219_image.nobg.png",
"20260626_143306_image.png",
"20260626_143200_image.png",
"20260626_143016_image.png",
"20260626_141714_7_20260626_141438_image.png",
"20260626_141657_6_20260626_141438_image.png",
"20260626_141649_image.png",
"20260626_141630_image.png",
"20260626_141608_3_20260626_141438_image.png",
"20260626_141553_2_20260626_141438_image.png",
"20260626_141534_1_20260626_141438_image.png",
"20260626_141518_0_20260626_141438_image.png",
"_turntable/up_5b23ff92/views/view_013_195deg.png",
"20260626_141438_image.png",
"_turntable/up_5b23ff92/views/view_012_180deg.png",
"_turntable/up_5b23ff92/views/view_011_165deg.png",
"_turntable/up_5b23ff92/views/view_010_150deg.png",
"_turntable/up_5b23ff92/views/view_009_135deg.png",
"_turntable/up_5b23ff92/views/view_008_120deg.png",
"_turntable/up_5b23ff92/views/view_007_105deg.png",
"_turntable/up_5b23ff92/views/view_006_090deg.png",
"_turntable/up_5b23ff92/views/view_005_075deg.png",
"_turntable/up_5b23ff92/views/view_004_060deg.png",
"_turntable/up_5b23ff92/views/view_003_045deg.png",
"_turntable/up_5b23ff92/views/view_002_030deg.png",
"_turntable/up_5b23ff92/views/view_001_015deg.png",
"_turntable/up_5b23ff92/views/view_000_000deg.png",
"_turntable/cg_ad73335e/views/view_023_345deg.png",
"_turntable/cg_ad73335e/views/view_018_270deg.png",
"_turntable/cg_ad73335e/views/view_017_255deg.png",
"_turntable/cg_ad73335e/views/view_016_240deg.png",
"_turntable/cg_ad73335e/views/view_015_225deg.png",
"_turntable/cg_ad73335e/views/view_014_210deg.png",
"_turntable/cg_ad73335e/views/view_013_195deg.png",
"_turntable/cg_ad73335e/views/view_012_180deg.png",
"_turntable/cg_ad73335e/views/view_011_165deg.png",
"_turntable/cg_ad73335e/views/view_010_150deg.png",
"_turntable/cg_ad73335e/views/view_009_135deg.png",
"_turntable/cg_ad73335e/views/view_008_120deg.png",
"_turntable/cg_ad73335e/views/view_007_105deg.png",
"_turntable/cg_ad73335e/views/view_006_090deg.png",
"_turntable/cg_ad73335e/views/view_005_075deg.png",
"_turntable/cg_ad73335e/views/view_004_060deg.png",
"_turntable/cg_ad73335e/views/view_003_045deg.png",
"_turntable/cg_ad73335e/views/view_000_000deg.png",
"_turntable/cg_61b863a8/views/view_023_345deg.png",
"_turntable/cg_61b863a8/views/view_022_330deg.png",
"_turntable/cg_61b863a8/views/view_021_315deg.png",
"_turntable/cg_61b863a8/views/view_020_300deg.png",
"_turntable/cg_61b863a8/views/view_019_285deg.png",
"_turntable/cg_61b863a8/views/view_018_270deg.png",
"_turntable/cg_61b863a8/views/view_017_255deg.png",
"_turntable/cg_61b863a8/views/view_016_240deg.png",
"_turntable/cg_61b863a8/views/view_015_225deg.png",
"_turntable/cg_61b863a8/views/view_014_210deg.png",
"_turntable/cg_61b863a8/views/view_013_195deg.png",
"_turntable/cg_61b863a8/views/view_012_180deg.png",
"_turntable/cg_61b863a8/views/view_011_165deg.png",
"_turntable/cg_61b863a8/views/view_010_150deg.png",
"_turntable/cg_61b863a8/views/view_009_135deg.png",
"_turntable/cg_61b863a8/views/view_008_120deg.png",
"_turntable/cg_61b863a8/views/view_007_105deg.png",
"_turntable/cg_61b863a8/views/view_006_090deg.png",
"_turntable/cg_61b863a8/views/view_005_075deg.png",
"_turntable/cg_61b863a8/views/view_004_060deg.png",
"_turntable/cg_61b863a8/views/view_003_045deg.png",
"_turntable/cg_61b863a8/views/view_002_030deg.png",
"_turntable/cg_61b863a8/views/view_001_015deg.png",
"_turntable/cg_61b863a8/views/view_000_000deg.png",
"_turntable/cg_f0049873/views/view_023_345deg.png",
"_turntable/cg_f0049873/views/view_022_330deg.png",
"_turntable/cg_f0049873/views/view_021_315deg.png",
"_turntable/cg_f0049873/views/view_020_300deg.png",
"_turntable/cg_f0049873/views/view_019_285deg.png",
"_turntable/cg_f0049873/views/view_018_270deg.png",
"_turntable/cg_f0049873/views/view_017_255deg.png",
"_turntable/cg_f0049873/views/view_016_240deg.png",
"_turntable/cg_f0049873/views/view_015_225deg.png",
"_turntable/cg_f0049873/views/view_014_210deg.png",
"_turntable/cg_f0049873/views/view_013_195deg.png",
"_turntable/cg_f0049873/views/view_012_180deg.png",
"_turntable/cg_f0049873/views/view_011_165deg.png",
"_turntable/cg_f0049873/views/view_010_150deg.png",
"_turntable/cg_f0049873/views/view_009_135deg.png",
"_turntable/cg_f0049873/views/view_008_120deg.png",
"_turntable/cg_f0049873/views/view_007_105deg.png",
"_turntable/cg_f0049873/views/view_006_090deg.png",
"_turntable/cg_f0049873/views/view_005_075deg.png",
"_turntable/cg_f0049873/views/view_004_060deg.png",
"_turntable/cg_f0049873/views/view_003_045deg.png",
"_turntable/cg_f0049873/views/view_002_030deg.png",
"_turntable/cg_f0049873/views/view_001_015deg.png",
"_turntable/cg_f0049873/views/view_000_000deg.png",
"_turntable/cg_08203e88/views/view_023_345deg.png",
"_turntable/cg_08203e88/views/view_022_330deg.png",
"_turntable/cg_08203e88/views/view_021_315deg.png",
"_turntable/cg_08203e88/views/view_020_300deg.png",
"_turntable/cg_08203e88/views/view_019_285deg.png",
"_turntable/cg_08203e88/views/view_018_270deg.png",
"_turntable/up_98c8443e/views/view_022_330deg.png",
"_turntable/up_98c8443e/views/view_021_315deg.png",
"_turntable/up_98c8443e/views/view_020_300deg.png",
"_turntable/up_98c8443e/views/view_019_285deg.png",
"_turntable/up_98c8443e/views/view_018_270deg.png",
"_turntable/up_98c8443e/views/view_017_255deg.png",
"_turntable/up_98c8443e/views/view_016_240deg.png",
"_turntable/up_98c8443e/views/view_015_225deg.png",
"_turntable/up_98c8443e/views/view_014_210deg.png",
"_turntable/up_98c8443e/views/view_013_195deg.png",
"_turntable/up_98c8443e/views/view_012_180deg.png",
"_turntable/up_98c8443e/views/view_011_165deg.png",
"_turntable/up_98c8443e/views/view_010_150deg.png",
"_turntable/up_98c8443e/views/view_009_135deg.png",
"_turntable/up_98c8443e/views/view_008_120deg.png",
"_turntable/up_98c8443e/views/view_007_105deg.png",
"_turntable/up_98c8443e/views/view_006_090deg.png",
"_turntable/up_98c8443e/views/view_005_075deg.png",
"_turntable/up_98c8443e/views/view_004_060deg.png",
"_turntable/up_98c8443e/views/view_003_045deg.png",
"_turntable/up_98c8443e/views/view_002_030deg.png",
"_turntable/up_98c8443e/views/view_001_015deg.png",
"up_98c8443e_face.png",
"up_810b587c_face.png",
"cg_7c7b22c9_face.png",
"20160903_200935.jpg_face.png",
"_turntable/cg_08203e88/views/view_017_255deg.png",
"_turntable/cg_08203e88/views/view_016_240deg.png",
"_turntable/cg_08203e88/views/view_015_225deg.png",
"_turntable/cg_08203e88/views/view_014_210deg.png",
"_turntable/cg_08203e88/views/view_013_195deg.png",
"_turntable/cg_08203e88/views/view_012_180deg.png",
"_turntable/cg_08203e88/views/view_011_165deg.png",
"_turntable/cg_08203e88/views/view_010_150deg.png",
"_turntable/cg_08203e88/views/view_009_135deg.png",
"_turntable/cg_08203e88/views/view_008_120deg.png",
"_turntable/cg_08203e88/views/view_007_105deg.png",
"_turntable/cg_08203e88/views/view_006_090deg.png",
"_turntable/cg_08203e88/views/view_005_075deg.png",
"_turntable/cg_08203e88/views/view_004_060deg.png",
"_turntable/cg_08203e88/views/view_003_045deg.png",
"_turntable/cg_08203e88/views/view_002_030deg.png",
"_turntable/cg_08203e88/views/view_001_015deg.png",
"_turntable/cg_08203e88/views/view_000_000deg.png",
"_turntable/up_3a05a2de/views/view_023_345deg.png",
"_turntable/up_3a05a2de/views/view_022_330deg.png",
"_turntable/up_3a05a2de/views/view_021_315deg.png",
"_turntable/up_3a05a2de/views/view_020_300deg.png",
"_turntable/up_3a05a2de/views/view_019_285deg.png",
"_turntable/up_3a05a2de/views/view_018_270deg.png",
"_turntable/up_3a05a2de/views/view_017_255deg.png",
"_turntable/up_3a05a2de/views/view_016_240deg.png",
"_turntable/up_3a05a2de/views/view_015_225deg.png",
"_turntable/up_3a05a2de/views/view_014_210deg.png",
"_turntable/up_3a05a2de/views/view_013_195deg.png",
"_turntable/up_3a05a2de/views/view_012_180deg.png",
"_turntable/up_3a05a2de/views/view_011_165deg.png",
"_turntable/up_3a05a2de/views/view_010_150deg.png",
"_turntable/up_3a05a2de/views/view_009_135deg.png",
"_turntable/up_3a05a2de/views/view_008_120deg.png",
"_turntable/up_3a05a2de/views/view_007_105deg.png",
"_turntable/up_3a05a2de/views/view_006_090deg.png",
"_turntable/up_3a05a2de/views/view_005_075deg.png",
"_turntable/up_3a05a2de/views/view_004_060deg.png",
"_turntable/up_3a05a2de/views/view_003_045deg.png",
"_turntable/up_3a05a2de/views/view_002_030deg.png",
"_turntable/up_3a05a2de/views/view_001_015deg.png",
"_turntable/up_3a05a2de/views/view_000_000deg.png",
"_turntable/up_b64786e7/views/view_023_345deg.png",
"_turntable/up_b64786e7/views/view_022_330deg.png",
"_turntable/up_b64786e7/views/view_021_315deg.png",
"_turntable/up_b64786e7/views/view_020_300deg.png",
"_turntable/up_b64786e7/views/view_019_285deg.png",
"_turntable/up_b64786e7/views/view_018_270deg.png",
"_turntable/up_b64786e7/views/view_017_255deg.png",
"_turntable/up_b64786e7/views/view_016_240deg.png",
"_turntable/up_b64786e7/views/view_015_225deg.png",
"_turntable/up_b64786e7/views/view_014_210deg.png",
"_turntable/up_b64786e7/views/view_013_195deg.png",
"_turntable/up_b64786e7/views/view_012_180deg.png",
"_turntable/up_b64786e7/views/view_011_165deg.png",
"_turntable/up_b64786e7/views/view_010_150deg.png",
"_turntable/up_b64786e7/views/view_009_135deg.png",
"_turntable/up_b64786e7/views/view_008_120deg.png",
"_turntable/up_b64786e7/views/view_007_105deg.png",
"_turntable/up_b64786e7/views/view_006_090deg.png",
"_turntable/up_b64786e7/views/view_005_075deg.png",
"_turntable/up_b64786e7/views/view_004_060deg.png",
"_turntable/up_b64786e7/views/view_003_045deg.png",
"_turntable/up_b64786e7/views/view_002_030deg.png",
"_turntable/up_b64786e7/views/view_001_015deg.png",
"_turntable/up_b64786e7/views/view_000_000deg.png",
"_turntable/up_ab858b0a/views/view_021_315deg.png",
"_turntable/up_ab858b0a/views/view_020_300deg.png",
"_turntable/up_ab858b0a/views/view_019_285deg.png",
"_turntable/up_ab858b0a/views/view_018_270deg.png",
"_turntable/up_ab858b0a/views/view_017_255deg.png",
"_turntable/up_ab858b0a/views/view_016_240deg.png",
"_turntable/up_ab858b0a/views/view_015_225deg.png",
"_turntable/up_ab858b0a/views/view_014_210deg.png",
"_turntable/up_ab858b0a/views/view_013_195deg.png",
"_turntable/up_ab858b0a/views/view_012_180deg.png",
"_turntable/up_ab858b0a/views/view_011_165deg.png",
"_turntable/up_ab858b0a/views/view_010_150deg.png",
"_turntable/up_ab858b0a/views/view_009_135deg.png",
"_turntable/up_ab858b0a/views/view_008_120deg.png",
"_turntable/up_ab858b0a/views/view_007_105deg.png",
"_turntable/up_ab858b0a/views/view_006_090deg.png",
"_turntable/up_ab858b0a/views/view_005_075deg.png",
"_turntable/up_ab858b0a/views/view_004_060deg.png",
"_turntable/up_ab858b0a/views/view_003_045deg.png",
"_turntable/up_ab858b0a/views/view_002_030deg.png",
"_turntable/up_ab858b0a/views/view_001_015deg.png",
"_turntable/up_ab858b0a/views/view_000_000deg.png",
"_turntable/up_f475c600/views/view_023_345deg.png",
"_turntable/up_f475c600/views/view_022_330deg.png",
"_turntable/up_f475c600/views/view_021_315deg.png",
"_turntable/up_f475c600/views/view_020_300deg.png",
"_turntable/up_f475c600/views/view_019_285deg.png",
"_turntable/up_f475c600/views/view_018_270deg.png",
"_turntable/up_f475c600/views/view_017_255deg.png",
"_turntable/up_f475c600/views/view_016_240deg.png",
"_turntable/up_f475c600/views/view_015_225deg.png",
"_turntable/up_f475c600/views/view_014_210deg.png",
"_turntable/up_f475c600/views/view_013_195deg.png",
"_turntable/up_f475c600/views/view_012_180deg.png",
"_turntable/up_f475c600/views/view_011_165deg.png",
"_turntable/up_f475c600/views/view_010_150deg.png",
"_turntable/up_f475c600/views/view_009_135deg.png",
"_turntable/up_f475c600/views/view_008_120deg.png",
"_turntable/up_f475c600/views/view_007_105deg.png",
"_turntable/up_f475c600/views/view_006_090deg.png",
"_turntable/up_f475c600/views/view_005_075deg.png",
"_turntable/up_f475c600/views/view_004_060deg.png",
"_turntable/up_f475c600/views/view_003_045deg.png",
"_turntable/up_f475c600/views/view_002_030deg.png",
"_turntable/up_f475c600/views/view_001_015deg.png",
"_turntable/up_f475c600/views/view_000_000deg.png",
"_turntable/up_810b587c/turntable.mp4",
"_turntable/up_810b587c/views/view_023_345deg.png",
"_turntable/up_810b587c/views/view_022_330deg.png",
"_turntable/up_810b587c/views/view_021_315deg.png",
"_turntable/up_810b587c/views/view_020_300deg.png",
"_turntable/up_810b587c/views/view_019_285deg.png",
"_turntable/up_810b587c/views/view_018_270deg.png",
"_turntable/up_810b587c/views/view_017_255deg.png",
"_turntable/up_810b587c/views/view_016_240deg.png",
"_turntable/up_810b587c/views/view_015_225deg.png",
"_turntable/up_810b587c/views/view_014_210deg.png",
"_turntable/up_810b587c/views/view_013_195deg.png",
"_turntable/up_810b587c/views/view_012_180deg.png",
"_turntable/up_810b587c/views/view_011_165deg.png",
"_turntable/up_810b587c/views/view_010_150deg.png",
"_turntable/up_810b587c/views/view_009_135deg.png",
"_turntable/up_810b587c/views/view_008_120deg.png",
"_turntable/up_810b587c/views/view_007_105deg.png",
"_turntable/up_810b587c/views/view_006_090deg.png",
"_turntable/up_810b587c/views/view_005_075deg.png",
"_turntable/up_810b587c/views/view_004_060deg.png",
"_turntable/up_810b587c/views/view_003_045deg.png",
"_turntable/up_810b587c/views/view_002_030deg.png",
"_turntable/up_810b587c/views/view_001_015deg.png",
"_turntable/up_810b587c/views/view_000_000deg.png",
"_turntable/img_77.png/views/view_023_345deg.png",
"_turntable/img_77.png/views/view_022_330deg.png",
"_turntable/img_77.png/views/view_021_315deg.png",
"_turntable/img_77.png/views/view_020_300deg.png",
"_turntable/img_77.png/views/view_019_285deg.png",
"_turntable/img_77.png/views/view_018_270deg.png",
"_turntable/img_77.png/views/view_017_255deg.png",
"_turntable/img_77.png/views/view_016_240deg.png",
"_turntable/img_77.png/views/view_015_225deg.png",
"_turntable/img_77.png/views/view_014_210deg.png",
"_turntable/img_77.png/views/view_013_195deg.png",
"_turntable/img_77.png/views/view_012_180deg.png",
"_turntable/img_77.png/views/view_011_165deg.png",
"_turntable/img_77.png/views/view_010_150deg.png",
"_turntable/img_77.png/views/view_009_135deg.png",
"_turntable/img_77.png/views/view_008_120deg.png",
"_turntable/img_77.png/views/view_007_105deg.png",
"_turntable/img_77.png/views/view_006_090deg.png",
"_turntable/img_77.png/views/view_005_075deg.png",
"_turntable/img_77.png/views/view_004_060deg.png",
"_turntable/img_77.png/views/view_003_045deg.png",
"_turntable/img_77.png/views/view_002_030deg.png",
"_turntable/img_77.png/views/view_001_015deg.png",
"_turntable/img_77.png/views/view_000_000deg.png",
"_turntable/cg_e79533a4/views/view_023_345deg.png",
"_turntable/cg_e79533a4/views/view_022_330deg.png",
"_turntable/cg_e79533a4/views/view_021_315deg.png",
"_turntable/cg_e79533a4/views/view_020_300deg.png",
"_turntable/cg_e79533a4/views/view_019_285deg.png",
"_turntable/cg_e79533a4/views/view_018_270deg.png",
"_turntable/cg_e79533a4/views/view_017_255deg.png",
"_turntable/cg_e79533a4/views/view_016_240deg.png",
"_turntable/cg_e79533a4/views/view_015_225deg.png",
"_turntable/cg_e79533a4/views/view_014_210deg.png",
"_turntable/cg_e79533a4/views/view_013_195deg.png",
"_turntable/cg_e79533a4/views/view_012_180deg.png",
"_turntable/cg_e79533a4/views/view_011_165deg.png",
"_turntable/cg_e79533a4/views/view_010_150deg.png",
"_turntable/cg_e79533a4/views/view_009_135deg.png",
"_turntable/cg_e79533a4/views/view_008_120deg.png",
"_turntable/cg_e79533a4/views/view_007_105deg.png",
"_turntable/cg_e79533a4/views/view_006_090deg.png",
"_turntable/cg_e79533a4/views/view_005_075deg.png",
"_turntable/cg_e79533a4/views/view_004_060deg.png",
"_turntable/cg_e79533a4/views/view_003_045deg.png",
"_turntable/cg_e79533a4/views/view_002_030deg.png",
"_turntable/cg_e79533a4/views/view_001_015deg.png",
"_turntable/cg_e79533a4/views/view_000_000deg.png",
"_turntable/b1.png/views/view_023_345deg.png",
"_turntable/b1.png/views/view_022_330deg.png",
"_turntable/b1.png/views/view_021_315deg.png",
"_turntable/b1.png/views/view_020_300deg.png",
"_turntable/b1.png/views/view_019_285deg.png",
"_turntable/up_e25ad102/views/view_011_330deg.png",
"_turntable/up_e25ad102/views/view_010_300deg.png",
"_turntable/up_e25ad102/views/view_009_270deg.png",
"_turntable/b1.png/views/view_016_240deg.png",
"_turntable/up_e25ad102/views/view_008_240deg.png",
"_turntable/up_e25ad102/views/view_007_210deg.png",
"_turntable/up_e25ad102/views/view_006_180deg.png",
"_turntable/up_e25ad102/views/view_005_150deg.png",
"_turntable/up_e25ad102/views/view_004_120deg.png",
"_turntable/up_e25ad102/views/view_003_090deg.png",
"_turntable/up_e25ad102/views/view_002_060deg.png",
"_turntable/up_e25ad102/views/view_001_030deg.png",
"_turntable/up_e25ad102/views/view_000_000deg.png",
"_turntable/b1.png/views/view_015_225deg.png",
"_turntable/b1.png/views/view_014_210deg.png",
"_turntable/b1.png/views/view_013_195deg.png",
"_turntable/b1.png/views/view_012_180deg.png",
"_turntable/b1.png/views/view_011_165deg.png",
"_turntable/b1.png/views/view_010_150deg.png",
"_turntable/b1.png/views/view_009_135deg.png",
"_turntable/b1.png/views/view_008_120deg.png",
"_turntable/b1.png/views/view_007_105deg.png",
"_turntable/b1.png/views/view_006_090deg.png",
"_turntable/b1.png/views/view_005_075deg.png",
"_turntable/b1.png/views/view_004_060deg.png",
"_turntable/b1.png/views/view_003_045deg.png",
"_turntable/b1.png/views/view_002_030deg.png",
"_turntable/b1.png/views/view_001_015deg.png",
"_turntable/b1.png/views/view_000_000deg.png",
"img_14.png_face.png",
"20260626_053719_pad_20260619_224737_img_14.nobg.png",
"20260626_053719_pad_20260619_224737_img_14.nobg.nobg.png",
"20260626_053754_pad_20260619_224737_img_14.png",
"20260626_053719_pad_20260619_224737_img_14.png",
"20260626_043734_7_20260618_053802_image.nobg.nobg.png",
"20260626_043915_crop_20260626_043734_7_20260618_053802_image.nobg.nobg.png",
"20260626_043734_7_20260618_053802_image.nobg.png",
"20260628_032038_dup_20260618_053932_7_20260618_053802_image.nobg.png",
"20260618_053932_7_20260618_053802_image.nobg.png",
"20260626_041550_sc_pad_20260626_034830_jb.nobg.png",
"20260626_041539_sc_pad_20260626_034830_jb.nobg.png",
"20260626_041515_sc_pad_20260626_034830_jb.nobg.png",
"20260626_041456_sc_pad_20260626_034830_jb.nobg.png",
"20260626_041410_sc_pad_20260626_034830_jb.nobg.png",
"20260626_041256_sc_pad_20260626_034830_jb.nobg.png",
"20260626_040655_sc_pad_20260626_034830_jb.nobg.png",
"20260626_040550_sc_pad_20260626_034830_jb.nobg.png",
"20260626_035131_pose_pad_20260626_034830_jb.nobg.png",
"20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260626_034830_jb.nobg.png",
"20260626_034830_jb.png",
"20260626_034708_jb.png",
"20260626_034601_jb.png",
"20260626_034455_jb.png",
"20260626_031810_1_20260618_050846_img_4.nobg.png",
"20260618_050923_2_20260618_050846_img_4.nobg.png",
"20260626_030908_1_20260618_050846_img_4.nobg.png",
"20260626_031821_1_20260618_050846_img_4.png",
"20260626_031810_1_20260618_050846_img_4.png",
"20260626_031759_jb1.png",
"20260626_031748_jb1.png",
"20260626_031736_jb1.png",
"20260626_031725_jb.png",
"20260626_031658_jb.png",
"20260626_030929_jb.png",
"20260626_030918_jb1.png",
"20260626_030908_1_20260618_050846_img_4.png",
"20260626_025708_2_20260625_205722_image.png",
"20260626_025338_pad_20260625_205913_8_20260625_205722_image.png",
"20260626_023528_pad_20260625_205913_8_20260625_205722_image.png",
"20260626_021942_pad_20260625_205913_8_20260625_205722_image.png",
"20260626_021533_pad_20260625_205828_4_20260625_205722_image.png",
"20260626_014422_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
"20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
"20260626_005504_Pasted image (9).nobg.png",
"20260626_005504_Pasted image (9).png",
"20260626_005644_dup_20260626_005504_Pasted image (9).png",
"20260626_005322_dup_20260626_003125_dup_Pasted image (9).png",
"20260626_003125_dup_Pasted image (9).png",
"cg_ad73335e_face.png",
"20260625_232346_sc_image.png",
"20260625_232242_sc_image.png",
"20260625_232204_sc_image.png",
"20260625_231601_sc_image.png",
"20260625_225929_sc_image.png",
"20260625_223304_p12.png",
"20260625_213330_pad_20260625_193323_image.png",
"20260625_210437_image.png",
"20260625_210432_image.png",
"20260625_210424_image.png",
"20260625_205925_9_20260625_205722_image.png",
"20260625_205913_8_20260625_205722_image.png",
"20260625_205902_7_20260625_205722_image.png",
"20260625_205851_6_20260625_205722_image.png",
"20260625_205839_5_20260625_205722_image.png",
"20260625_205828_4_20260625_205722_image.png",
"20260625_205817_3_20260625_205722_image.png",
"20260625_205806_2_20260625_205722_image.png",
"20260625_205756_1_20260625_205722_image.png",
"20260625_205745_0_20260625_205722_image.png",
"20260625_205722_image.png",
"20260625_194224_image.png",
"20260625_193852_pose_image.png",
"20260625_193744_sc_image.png",
"20260625_193526_image.png",
"up_fe1de29e_face.png",
"20260625_193345_image.png",
"20260625_193334_image.png",
"20260625_193323_image.png",
"20260625_193258_mr_image.png",
"20260625_193157_image.png",
"20260625_192755_image.png",
"20260625_192722_9_20260625_192510_image.png",
"20260625_192711_8_20260625_192510_image.png",
"20260625_192659_7_20260625_192510_image.png",
"20260625_192646_6_20260625_192510_image.png",
"20260625_192557_3_20260625_192510_image.png",
"20260625_192549_image.png",
"20260625_192543_2_20260625_192510_image.png",
"20260625_192532_1_20260625_192510_image.png",
"20260625_192521_0_20260625_192510_image.png",
"20260625_192510_image.png",
"20260625_191944_8_20260625_191641_image.png",
"20260625_191932_7_20260625_191641_image.png",
"20260625_191921_6_20260625_191641_image.png",
"20260625_191910_5_20260625_191641_image.png",
"20260625_191905_Screenshot_From_2026-06-25_19-15-26.png",
"20260625_191848_3_20260625_191641_image.png",
"20260625_191837_2_20260625_191641_image.png",
"20260625_191827_1_20260625_191641_image.png",
"20260625_191816_0_20260625_191641_image.png",
"20260625_191641_image.png",
"20260625_051219_sc_3_20260618_173728_image.png",
"20260625_050959_sc_3_20260618_173728_image.png",
"20260625_050729_sc_3_20260618_173728_image.png",
"20260625_050554_sc_3_20260618_173728_image.png",
"20260625_050458_sc_3_20260618_173728_image.png",
"20260625_050259_sc_3_20260618_173728_image.png",
"20260625_050157_sc_3_20260618_173728_image.png",
"20260625_045904_sc_3_20260618_173728_image.png",
"20260618_173728_image.png_face.png",
"20260625_045029_pose_3_20260618_173728_image.png",
"20260625_044738_pose_pose_pose_3_20260618_173728_image.png",
"20260625_044655_pose_pose_3_20260618_173728_image.png",
"20260625_044615_pose_3_20260618_173728_image.png",
"20260625_042243_5_20260621_205151_image.png",
"20260625_042116_image.png",
"20260625_041840_image.png",
"20260625_041714_image.png",
"20260625_041647_7_20260621_205151_image.png",
"20260625_041514_7_20260621_205151_image.png",
"20260625_040236_pose_sc_image.png",
"20260625_032521_dup_20260625_032449_sc_image.png",
"20260625_032449_sc_image.nobg.png",
"up_f9a5bb33_face.png",
"20260625_032449_sc_image.png",
"20260625_034504_dup_20260625_032449_sc_image.png",
"20260625_032341_sc_image.png",
"20260625_032146_sc_image.png",
"20260625_032107_image.png",
"20260625_031345_dup_20260625_031229_sc_image.png",
"20260625_031229_sc_image.png",
"20260625_030956_sc_image.png",
"20260625_030904_sc_image.png",
"20260625_030526_sc_image.png",
"20260625_030438_image.png",
"20260625_025228_dup_20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_025156_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_025115_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_025335_dup_20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_023536_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260624_165109_0_20260622_091731_image.nobg.png",
"20260624_165109_0_20260622_091731_image.png",
"20260625_023536_dup_20260624_165109_0_20260622_091731_image.png",
"up_ab858b0a_face.png",
"20260625_022332_dup_20260625_022250_sc_image.png",
"20260625_022250_sc_image.png",
"20260625_022250_sc_image.nobg.png",
"20260625_022109_sc_image.png",
"20260625_022007_sc_image.png",
"20260625_021910_image.png",
"20260625_021702_sc_image.png",
"20260625_021615_sc_image.png",
"20260625_015711_sc_image.png",
"20260625_020212_dup_20260625_015711_sc_image.png",
"20260625_015711_sc_image.nobg.png",
"20260625_015900_sc_image.png",
"20260625_015413_sc_image.png",
"20260625_015213_image.png",
"up_a1dddf6f_face.png",
"20260625_014914_sc_mr_mr_image.nobg.png",
"20260625_014212_sc_0_20260619_124038_image.png",
"20260625_014026_sc_0_20260619_124038_image.png",
"20260625_013902_sc_0_20260619_124038_image.png",
"20260625_013708_sc_0_20260619_124038_image.png",
"20260625_013515_sc_0_20260619_124038_image.png",
"20260625_012538_7_20260622_101056_image.png",
"20260625_012527_7_20260622_101056_image.png",
"20260625_012516_7_20260622_101056_image.png",
"20260625_012442_7_20260622_101056_image.nobg.png",
"20260625_012420_7_20260622_101056_image.png",
"20260625_012357_2_20260622_101056_image.png",
"20260625_012346_2_20260622_101056_image.png",
"20260625_012335_2_20260622_101056_image.png",
"20260625_011955_fs_badly_1039s-1109s_0s-17s_mr_image.nobg.png_prev50.mp4",
"20260625_011802_sc_mr_image.nobg.png",
"20260625_005004_sc_mr_image.nobg.png",
"20260625_004141_sc_sc_mr_image.nobg.png",
"20260625_003214_sc_image.nobg.png",
"20260625_001534_sc_mr_image.nobg.png",
"20260625_001215_sc_mr_image.nobg.png",
"20260625_001047_sc_mr_image.nobg.png",
"20260625_000950_sc_mr_image.nobg.png",
"20260625_000851_sc_mr_image.nobg.png",
"20260625_000441_sc_mr_image.nobg.png",
"20260625_000430_mr_mr_image.nobg.png",
"20260625_000257_sc_mr_image.nobg.png",
"20260624_235623_sc_mr_image.nobg.png",
"20260624_235607_mr_sc_mr_image.nobg.png",
"20260624_234643_sc_mr_image.nobg.png",
"20260624_234358_sc_image.nobg.png",
"20260624_234309_mr_mr_image.nobg.png",
"20260624_233935_sc_mr_image.nobg.png",
"20260624_233903_mr_mr_image.nobg.png",
"20260624_233746_sc_mr_image.nobg.png",
"20260624_233254_sc_image.nobg.png",
"cg_6f321af3_face.png",
"20260624_224315_2_20260622_101056_image.png",
"20260624_224039_mr_image.nobg.png",
"20260624_224028_mr_image.nobg.png",
"20260624_224017_mr_image.nobg.png",
"20260624_224005_mr_image.nobg.png",
"20260624_223954_mr_image.nobg.png",
"20260624_223944_mr_image.nobg.png",
"20260624_223926_mr_image.nobg.png",
"20260624_220323_mr_image.nobg.png",
"20260624_205941_image.png",
"20260624_205924_image.png",
"20260624_205906_image.png",
"20260624_205638_image.png",
"20260624_205626_image.png",
"20260624_205615_image.png",
"20260624_203915_image.png",
"20260624_203904_image.png",
"20260624_203853_image.png",
"20260624_203510_image.png",
"up_52dede0c_face.png",
"20260624_202851_image.png",
"20260624_202831_image.png",
"20260624_202804_image.png",
"20260624_200205_0_20260619_124038_image.png",
"20260624_200155_image.png",
"20260624_195836_mr_mr_image.png",
"20260624_195729_mr_0_20260619_124038_image.png",
"20260624_195615_mr_cg_6f321af3_face.png",
"20260624_195448_mr_image.png",
"20260624_195054_image.png",
"20260624_194520_7_20260622_101056_image.png",
"20260624_194509_7_20260622_101056_image.png",
"20260624_194453_7_20260622_101056_image.png",
"20260624_194438_7_20260622_101056_image.png",
"20260624_194426_7_20260622_101056_image.png",
"20260624_194409_7_20260622_101056_image.png",
"20260624_194353_7_20260622_101056_image.png",
"20260624_194339_7_20260622_101056_image.png",
"20260624_194322_7_20260622_101056_image.png",
"20260624_194305_7_20260622_101056_image.png",
"20260624_194247_7_20260622_101056_image.png",
"20260624_194231_7_20260622_101056_image.png",
"20260624_193539_7_20260622_101056_image.png",
"20260624_193522_7_20260622_101056_image.png",
"20260624_193506_7_20260622_101056_image.png",
"20260624_193449_7_20260622_101056_image.png",
"20260624_193415_7_20260622_101056_image.png",
"20260624_193359_7_20260622_101056_image.png",
"20260624_193342_7_20260622_101056_image.png",
"20260624_193326_7_20260622_101056_image.png",
"20260624_193310_7_20260622_101056_image.png",
"20260624_193252_7_20260622_101056_image.png",
"20260622_101448_7_20260622_101056_image.png",
"20260624_193236_7_20260622_101056_image.png",
"20260624_193219_7_20260622_101056_image.png",
"20260624_193202_7_20260622_101056_image.png",
"20260624_193147_7_20260622_101056_image.png",
"20260624_193130_7_20260622_101056_image.png",
"20260624_193113_7_20260622_101056_image.png",
"20260624_193056_7_20260622_101056_image.png",
"20260624_193039_7_20260622_101056_image.png",
"20260622_101204_2_20260622_101056_image.png",
"20260624_192733_mr_image.nobg.png",
"20260624_192717_mr_image.nobg.png",
"20260624_192706_mr_image.nobg.png",
"20260624_192650_mr_image.nobg.png",
"20260624_192634_mr_image.nobg.png",
"20260624_192616_mr_image.nobg.png",
"20260624_192559_mr_image.nobg.png",
"20260624_192542_mr_image.nobg.png",
"20260624_192524_mr_image.nobg.png",
"20260624_192507_mr_image.nobg.png",
"20260624_192451_mr_image.nobg.png",
"20260624_192434_mr_image.nobg.png",
"20260624_192418_mr_image.nobg.png",
"20260624_192402_mr_image.nobg.png",
"20260624_192345_mr_image.nobg.png",
"20260624_192256_mr_image.nobg.png",
"20260624_192239_mr_image.nobg.png",
"20260624_192135_mr_image.nobg.png",
"20260624_192030_mr_image.nobg.png",
"20260624_191944_mr_image.nobg.png",
"20260624_191905_mr_image.nobg.png",
"20260624_191751_mr_image.nobg.png",
"20260624_191625_mr_image.nobg.png",
"20260624_191535_mr_image.nobg.png",
"20260624_191436_mr_image.nobg.png",
"20260624_191419_mr_image.nobg.png",
"20260624_191402_mr_image.nobg.png",
"20260624_191345_mr_image.nobg.png",
"20260624_191312_mr_image.nobg.png",
"20260624_191238_mr_image.nobg.png",
"20260624_191148_mr_image.nobg.png",
"20260624_191131_mr_image.nobg.png",
"20260624_191115_mr_image.nobg.png",
"20260624_191058_mr_image.nobg.png",
"20260624_191041_mr_image.nobg.png",
"20260624_190921_mr_image.nobg.png",
"20260624_190812_mr_image.nobg.png",
"20260624_190652_mr_image.nobg.png",
"20260624_190559_mr_image.png",
"20260624_190401_mr_image.nobg.png",
"20260624_190350_mr_image.nobg.png",
"20260624_190339_mr_image.nobg.png",
"20260624_190328_mr_image.nobg.png",
"20260624_190316_mr_image.nobg.png",
"20260624_190254_mr_image.nobg.png",
"20260624_190231_mr_image.nobg.png",
"20260624_190220_mr_image.nobg.png",
"20260624_190209_mr_image.nobg.png",
"20260624_190158_mr_image.nobg.png",
"20260624_190146_mr_image.nobg.png",
"20260624_190135_mr_image.nobg.png",
"20260624_190124_mr_image.nobg.png",
"20260624_190113_mr_image.nobg.png",
"20260624_190101_mr_image.nobg.png",
"20260624_190050_mr_image.nobg.png",
"20260624_190039_mr_image.nobg.png",
"20260624_190027_mr_image.nobg.png",
"20260624_190016_mr_image.nobg.png",
"20260624_190005_mr_image.nobg.png",
"20260624_185954_mr_image.nobg.png",
"20260624_185827_mr_image.nobg.png",
"20260624_183919_mr_image.png",
"20260624_183855_mr_image.png",
"20260624_183830_mr_image.png",
"20260624_183806_mr_image.png",
"20260624_183741_mr_image.png",
"20260624_183717_mr_image.png",
"20260624_183652_mr_image.png",
"20260624_183628_mr_image.png",
"20260624_183500_image.png",
"20260624_183211_Screenshot_From_2026-06-24_18-23-06.png",
"20260624_183148_9_20260624_182958_image.png",
"20260624_183137_8_20260624_182958_image.png",
"20260624_183126_7_20260624_182958_image.png",
"20260624_183104_5_20260624_182958_image.png",
"20260624_183053_4_20260624_182958_image.png",
"20260624_183042_3_20260624_182958_image.png",
"20260624_183031_2_20260624_182958_image.png",
"20260624_183021_1_20260624_182958_image.png",
"20260624_183010_0_20260624_182958_image.png",
"20260624_182958_image.png",
"20260624_174900_image.png",
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
"20260624_173115_image.png",
"20260624_174943_dup_20260624_173115_image.png",
"20260624_174757_image.png",
"20260624_174442_image.png",
"20260624_172923_image.nobg.png",
"20260624_170638_image.nobg.png",
"20260624_170210_image.nobg.png",
"20260624_165804_image.nobg.png",
"20260624_165804_image.png",
"20260624_165624_image.png",
"20260624_165429_image.png",
"20260624_165311_image.png",
"20260624_164917_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164534_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164523_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164512_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164501_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164449_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164438_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164427_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164404_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164353_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_162601_mr_0_20260622_093058_image.nobg.png",
"20260624_164240_crop_20260624_162342_1_20260622_093058_image.png",
"20260624_164218_crop_20260624_162601_mr_0_20260622_093058_image.nobg.png",
"20260624_164059_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164116_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_163634_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_162510_image.png",
"20260624_162408_image.png",
"20260624_162342_1_20260622_093058_image.png",
"20260624_160808_fs_badly_1039s-1109s_0_20260622_091731_image.png_prev50.mp4",
"20260624_160423_1_20260619_040135_image.png",
"20260624_160412_1_20260619_040135_image.png",
"20260624_160400_1_20260619_040135_image.png",
"20260624_160349_1_20260619_040135_image.png",
"20260624_160338_1_20260619_040135_image.png",
"20260624_160327_1_20260619_040135_image.png",
"20260624_160305_1_20260619_040135_image.png",
"20260624_160254_1_20260619_040135_image.png",
"20260624_160242_1_20260619_040135_image.png",
"20260624_160231_1_20260619_040135_image.png",
"20260624_160220_1_20260619_040135_image.png",
"20260624_160209_1_20260619_040135_image.png",
"20260624_160157_1_20260619_040135_image.png",
"20260624_160146_1_20260619_040135_image.png",
"20260624_160135_1_20260619_040135_image.png",
"20260624_160124_1_20260619_040135_image.png",
"20260624_160113_1_20260619_040135_image.png",
"20260624_160102_1_20260619_040135_image.png",
"20260624_154852_img_72.png",
"20260624_154840_img_72.png",
"20260624_154821_img_72.png",
"20260624_154802_img_72.png",
"20260624_154746_img_72.png",
"20260624_154729_img_72.png",
"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",
"20260624_154453_img_72.png",
"20260624_154442_img_72.png",
"20260624_154318_img_72.png",
"20260624_154236_img_72.png",
"20260624_154125_img_72.png",
"20260624_153940_img_72.png",
"20260624_153831_img_72.png",
"20260624_153614_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153417_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153344_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153102_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153051_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153040_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153029_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153018_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_153006_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152955_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152944_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152922_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152911_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152900_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152838_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152826_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152815_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152804_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152753_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152742_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152731_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152720_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152710_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152659_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152334_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152317_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152244_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152228_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152212_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152155_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152138_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152123_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152112_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152100_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152049_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152038_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_152027_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143523_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151746_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151501_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151439_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151250_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151239_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151228_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151216_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151205_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151143_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151131_Screenshot_From_2026-06-17_07-41-27.nobg.png",
"20260624_151053_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143727_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143714_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143700_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143647_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143635_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151613_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_144952_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143024_mr_image.png",
"20260624_142959_mr_image.png",
"20260624_142923_mr_image.png",
"20260624_142856_mr_image.png",
"20260624_142832_mr_image.png",
"20260624_142622_img_19_2.png",
"20260624_142611_image.png",
"20260624_142600_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_142509_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_142457_img_19_2.png",
"20260624_142445_image.png",
"20260624_141325_img_19_2.png",
"20260624_140813_Screenshot_From_2026-06-17_07-43-19.png",
"20260624_140807_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_140605_image.png",
"20260624_140532_image.png",
"20260624_140221_image.png",
"20260624_140129_image.png",
"20260624_135959_image.png",
"20260624_135912_image.png",
"20260619_194418_test123.nobg.png",
"20260624_134855_dup_20260624_055327_image.nobg.png",
"20260624_134836_dup_20260624_055327_image.nobg.png",
"20260624_125705_dup_20260624_055327_image.png",
"20260624_125526_dup_20260624_055327_image.nobg.png",
"20260624_123646_image.png",
"20260624_123608_image.png",
"20260624_121517_image.png",
"20260624_121506_image.png",
"20260624_121454_image.png",
"20260624_121439_image.png",
"20260624_121422_image.png",
"20260624_121353_image.png",
"20260624_121336_image.png",
"20260624_121320_image.png",
"20260624_121247_image.png",
"20260624_121231_image.png",
"20260624_121214_image.png",
"20260624_121140_image.png",
"20260624_121124_image.png",
"20260624_121003_mr_image.png",
"20260624_120932_mr_image.png",
"20260624_120916_mr_image.png",
"20260624_120859_mr_image.png",
"20260624_120842_mr_image.png",
"20260624_120825_mr_image.png",
"20260624_120809_mr_image.png",
"20260624_120520_mr_image.png",
"20260624_120503_mr_image.png",
"20260624_120446_mr_image.png",
"20260624_120430_mr_image.png",
"20260624_120414_mr_image.png",
"20260624_120135_mr_image.png",
"20260624_120119_mr_image.png",
"20260624_114651_mr_image.png",
"20260624_114639_mr_image.png",
"20260624_114607_mr_image.png",
"up_8cf9e62e_face.png",
"20260624_114501_mr_image.png",
"20260624_114430_mr_image.png",
"20260624_114300_mr_image.png",
"20260624_114039_mr_image.png",
"20260624_114009_mr_image.png",
"20260624_113931_mr_image.png",
"20260624_113819_image.png",
"20260624_113748_image.png",
"20260624_113717_image.png",
"20260624_113644_image.png",
"20260624_113240_image.png",
"20260624_113229_image.png",
"20260624_113217_image.png",
"20260624_113127_image.png",
"20260624_113039_image.png",
"20260624_112942_image.png",
"20260624_112852_image.png",
"20260624_110950_image.png",
"20260624_110613_image.png",
"20260624_110551_image.png",
"20260624_110443_image.png",
"20260624_110310_image.png",
"20260624_110253_image.png",
"20260624_110237_image.png",
"20260624_110158_mr_image.png",
"20260624_110028_image.png",
"20260624_110008_image.png",
"20260624_105859_5_20260624_105736_image.png",
"20260624_105813_2_20260624_105736_image.png",
"20260624_105757_1_20260624_105736_image.png",
"20260624_105736_image.png",
"20260624_103951_8_20260624_103246_image.nobg.png",
"20260624_103908_7_20260624_103246_image.nobg.png",
"20260624_104609_mr_1_20260624_103353_image.png",
"20260624_104059_image.png",
"20260624_104023_image.png",
"20260624_103940_7_20260624_103353_image.png",
"20260624_103919_5_20260624_103449_image.png",
"20260624_103908_7_20260624_103246_image.png",
"20260624_103846_6_20260624_103310_Screenshot_From_2026-06-24_10-30-32.png",
"20260624_103814_5_20260624_103353_image.png",
"20260624_103803_5_20260624_103310_Screenshot_From_2026-06-24_10-30-32.png",
"20260624_103742_5_20260624_103246_image.png",
"20260624_103642_4_20260624_103246_image.png",
"20260624_103609_1_20260624_103449_image.png",
"20260624_103601_3_20260624_103246_image.png",
"20260624_103547_2_20260624_103353_image.png",
"20260624_103536_2_20260624_103310_Screenshot_From_2026-06-24_10-30-32.png",
"20260624_103515_2_20260624_103246_image.png",
"20260624_103504_1_20260624_103353_image.png",
"20260624_103454_1_20260624_103310_Screenshot_From_2026-06-24_10-30-32.png",
"20260624_103449_image.png",
"20260624_103443_1_20260624_103246_image.png",
"20260624_103422_0_20260624_103310_Screenshot_From_2026-06-24_10-30-32.png",
"20260624_103411_0_20260624_103246_image.png",
"20260624_103353_image.png",
"20260624_103310_Screenshot_From_2026-06-24_10-30-32.png",
"20260624_103246_image.png",
"20260624_075439_0_20260622_101128_image.png",
"20260624_075405_0_20260622_101128_image.png",
"20260624_075343_0_20260622_101128_image.png",
"20260624_075331_0_20260622_101128_image.png",
"20260624_075258_0_20260622_101128_image.png",
"20260624_075244_0_20260622_101128_image.png",
"20260624_074849_0_20260622_101128_image.png",
"20260624_074835_0_20260622_101128_image.png",
"20260624_074818_0_20260622_101128_image.png",
"20260624_074801_0_20260622_101128_image.png",
"20260624_074659_5_20260622_101128_image.png",
"20260624_074623_5_20260622_101128_image.png",
"20260624_074536_2_20260622_101128_image.png",
"20260624_074423_0_20260622_101128_image.png",
"20260624_074303_2_20260622_101128_image.png",
"20260624_074154_0_20260622_101128_image.png",
"20260624_073959_2_20260622_101128_image.nobg.png",
"20260624_073907_2_20260622_101128_image.nobg.png",
"20260624_073757_2_20260622_101128_image.png",
"20260624_073656_2_20260622_101128_image.png",
"20260624_073634_2_20260622_101128_image.nobg.png",
"20260624_073429_image.nobg.png",
"20260624_073305_image.nobg.png",
"20260624_073222_7_20260622_101056_image.png",
"20260624_073100_7_20260622_101056_image.png",
"20260624_072848_7_20260622_101056_image.png",
"20260624_072658_7_20260622_101056_image.png",
"20260624_072406_image.nobg.png",
"20260624_072050_image.nobg.png",
"20260624_070918_image.nobg.png",
"20260624_070038_image.nobg.png",
"20260624_062056_image.nobg.png",
"20260624_055327_image.png",
"20260624_055134_image.png",
"20260624_055049_image.png",
"20260624_054816_image.png",
"20260624_054549_image.png",
"20260624_054134_image.png",
"20260624_054006_image.png",
"20260624_053727_mr_image.png",
"20260624_053637_mr_image.png",
"20260624_053541_mr_image.png",
"20260624_053439_mr_image.png",
"20260624_053147_mr_image.png",
"20260624_052826_others.jpeg",
"20260624_052755_mr_image.png",
"20260624_051049_others.jpeg",
"20260624_051003_others.jpeg",
"20260624_050750_others.jpeg",
"20260624_050145_mr_image.png",
"20260624_050134_mr_image.png",
"20260624_050124_mr_image.png",
"20260624_045145_mr_image.nobg.png",
"20260624_045240_mr_image.png",
"20260624_045145_mr_image.png",
"20260624_042747_mr_image.png",
"20260624_044343_mr_image.png",
"20260624_044054_mr_image.nobg.png",
"20260624_043937_mr_image.png",
"20260624_043843_mr_image.png",
"20260624_043811_mr_image.png",
"20260624_043304_mr_image.png",
"20260624_043253_mr_image.png",
"20260624_042912_mr_image.png",
"20260624_042437_mr_test123.jpeg",
"20260624_042249_image.png",
"20260624_042157_image.png",
"20260624_041946_others.jpeg",
"20260624_041847_0_20260623_164608_Screenshot_From_2026-06-23_16-43-24.png",
"20260624_041552_0_20260623_164608_Screenshot_From_2026-06-23_16-43-24.png",
"20260624_041453_mr_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_041230_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_041219_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_041006_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_040539_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_040502_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_035837_Screenshot_From_2026-06-23_16-38-43.png",
"20260624_035826_Screenshot_From_2026-06-23_16-38-43.png",
"20260624_035804_Screenshot_From_2026-06-23_16-38-43.png",
"20260624_035743_Screenshot_From_2026-06-23_16-38-43.png",
"20260624_035620_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_035609_Screenshot_From_2026-06-23_16-43-13.png",
"20260624_032316_fs_SlaapKamer_7_20260619_202903_image.png_prev50.mp4",
"20260624_031522_0_20260622_173855_image.nobg.png",
"20260624_031522_0_20260622_173855_image.png",
"20260624_025922_0_20260622_173855_image.png",
"20260624_025623_0_20260622_173855_image.png",
"20260624_024332_0_20260622_173855_image.png",
"20260624_024321_0_20260622_173855_image.png",
"20260624_024310_0_20260622_173855_image.png",
"20260624_024225_0_20260622_173855_image.png",
"20260624_024054_0_20260622_173855_image.png",
"20260624_024043_0_20260622_173855_image.png",
"20260624_024032_0_20260622_173855_image.png",
"20260624_024020_0_20260622_173855_image.png",
"20260624_024009_0_20260622_173855_image.png",
"20260624_023859_0_20260622_173855_image.png",
"20260624_023847_0_20260622_173855_image.png",
"20260624_023813_0_20260622_173855_image.png",
"20260624_023614_0_20260622_173855_image.png",
"20260624_023502_image.png",
"20260624_023451_image.png",
"20260624_023428_image.png",
"20260624_023417_image.png",
"20260624_023355_image.png",
"20260624_023332_image.png",
"20260624_023310_image.png",
"20260624_023259_image.png",
"20260624_023247_image.png",
"20260624_023236_image.png",
"20260624_023225_image.png",
"20260624_023214_image.png",
"20260624_023203_image.png",
"20260624_023127_image.png",
"20260624_023048_image.png",
"20260624_022956_image.png",
"20260624_022943_image.png",
"20260624_022930_image.png",
"20260624_022917_image.png",
"20260624_022904_image.png",
"20260624_022744_image.png",
"20260624_022731_image.png",
"20260624_022717_image.png",
"20260624_022652_image.png",
"20260624_022639_image.png",
"20260624_022533_image.png",
"20260624_022415_image.png",
"20260624_022401_image.png",
"20260624_022349_image.png",
"20260624_022029_image.png",
"20260624_022017_image.png",
"20260624_021938_image.png",
"20260624_021926_image.png",
"20260624_021913_image.png",
"20260624_021900_image.png",
"20260624_021718_image.png",
"20260624_021705_image.png",
"20260624_021640_image.png",
"20260624_021614_image.png",
"20260624_021535_image.png",
"20260624_021523_image.png",
"20260624_020935_image.png",
"20260624_020924_image.png",
"20260624_020913_image.png",
"20260624_020902_image.png",
"20260624_020817_image.png",
"20260624_020806_image.png",
"20260624_020755_image.png",
"20260624_020734_image.png",
"20260624_020712_image.png",
"20260624_020701_image.png",
"20260624_020616_image.png",
"20260624_020413_image.png",
"20260624_020224_image.png",
"20260624_020008_image.png",
"20260624_014136_image.png",
"20260624_014107_image.png",
"20260624_011624_image.png",
"20260624_011613_image.png",
"20260624_010711_0_20260624_005912_image.png",
"20260624_010318_image.png",
"20260624_010056_8_20260624_005912_image.png",
"20260624_010033_6_20260624_005912_image.png",
"20260624_010022_5_20260624_005912_image.png",
"20260624_010011_4_20260624_005912_image.png",
"20260624_010000_3_20260624_005912_image.png",
"20260624_005946_2_20260624_005912_image.png",
"20260624_005935_1_20260624_005912_image.png",
"20260624_005925_0_20260624_005912_image.png",
"20260624_005912_image.png",
"20260624_001219_image.png",
"20260623_165512_2_20260623_164608_Screenshot_From_2026-06-23_16-43-24.png",
"20260623_165429_Screenshot_From_2026-06-23_16-43-24.png",
"20260623_164857_2_20260623_164608_Screenshot_From_2026-06-23_16-43-24.png",
"20260623_164712_Screenshot_From_2026-06-23_16-38-43.png",
"20260623_164707_Screenshot_From_2026-06-23_16-39-09.png",
"20260623_164702_Screenshot_From_2026-06-23_16-40-05.png",
"20260623_164657_Screenshot_From_2026-06-23_16-41-48.png",
"20260623_164653_Screenshot_From_2026-06-23_16-42-52.png",
"20260623_164648_Screenshot_From_2026-06-23_16-43-13.png",
"20260623_164608_Screenshot_From_2026-06-23_16-43-24.png",
"20260623_003039_0_20260622_173855_image.png",
"20260623_001759_0_20260622_173855_image.png",
"20260623_001510_0_20260622_173855_image.png",
"20260623_001411_0_20260622_173855_image.png",
"20260623_001233_0_20260622_173855_image.png",
"20260623_001041_0_20260622_173855_image.png",
"20260622_235333_fs_badly_949s-1038s_1_20260622_193118_image.png_prev50.mp4",
"20260622_235229_fs_friend_260s-273s_image.png.mp4",
"20260622_234344_image.png",
"20260622_234205_image.png",
"20260622_233849_image.png",
"20260622_233522_image.png",
"20260622_233511_image.png",
"20260622_233500_image.png",
"20260622_233439_image.png",
"20260622_232330_fs_badly_1039s-1109s_image.png_prev50.mp4",
"20260622_231728_fs_mp4_sd_973s-1077s_image.png_prev50.mp4",
"20260622_231602_image.png",
"20260622_231551_image.png",
"20260622_231539_image.png",
"20260622_231528_image.png",
"20260622_231454_image.png",
"20260622_231444_image.png",
"20260622_231432_image.png",
"20260622_231422_image.png",
"20260622_231034_image.png",
"20260622_231017_image.png",
"20260622_231000_image.png",
"20260622_230944_image.png",
"20260622_230927_image.png",
"20260622_230910_image.png",
"20260622_230841_image.png",
"20260622_230756_image.png",
"20260622_230600_image.png",
"20260622_230538_image.png",
"20260622_230431_image.png",
"20260622_230421_image.png",
"20260622_230059_image.png",
"20260622_225422_image.png",
"20260622_225349_image.png",
"20260622_225321_image.png",
"20260622_225310_image.png",
"20260622_224723_image.png",
"20260622_224711_image.png",
"20260622_224700_image.png",
"20260622_224649_image.png",
"20260622_224638_image.png",
"20260622_224627_image.png",
"20260622_224616_image.png",
"20260622_224605_image.png",
"20260622_224554_image.png",
"20260622_224543_image.png",
"20260622_224532_image.png",
"20260622_224521_image.png",
"20260622_224457_image.png",
"20260622_224434_image.png",
"20260622_224402_image.png",
"20260622_224001_image.png",
"20260622_223642_image.png",
"20260622_223622_2_20260622_193118_image.png",
"20260622_223315_image.png",
"20260622_222138_image.png",
"20260622_222119_0_20260619_124038_image.png",
"20260622_221407_0_20260619_124038_image.png",
"20260622_221350_0_20260619_124038_image.png",
"20260622_221333_0_20260619_124038_image.png",
"20260622_221317_0_20260619_124038_image.png",
"20260622_221300_0_20260619_124038_image.png",
"20260622_221244_0_20260619_124038_image.png",
"20260622_221228_0_20260619_124038_image.png",
"20260622_215853_0_20260619_124038_image.png",
"20260622_215842_0_20260619_124038_image.png",
"20260622_215831_0_20260619_124038_image.png",
"20260622_215820_0_20260619_124038_image.png",
"20260622_215703_dup_20260622_181910_0_20260619_124038_image.png",
"20260622_205918_0_20260619_124038_image.png",
"20260622_205853_0_20260619_124038_image.png",
"20260622_205722_0_20260619_124038_image.png",
"20260622_205705_0_20260619_124038_image.png",
"20260622_205648_0_20260619_124038_image.png",
"20260622_205522_0_20260619_124038_image.png",
"20260622_193821_Pasted_image__2_.png",
"20260622_193813_Pasted_image.png",
"20260622_193601_7_20260622_193118_image.png",
"20260622_193544_6_20260622_193118_image.png",
"20260622_193527_5_20260622_193118_image.png",
"20260622_193428_2_20260622_193118_image.png",
"20260622_193411_1_20260622_193118_image.png",
"20260622_193354_0_20260622_193118_image.png",
"20260622_193118_image.png",
"20260622_212012_dup_20260622_181910_0_20260619_124038_image.png",
"20260622_181910_0_20260619_124038_image.png",
"20260622_181734_0_20260619_124038_image.png",
"20260622_181720_0_20260619_124038_image.png",
"20260622_181634_0_20260619_124038_image.png",
"20260622_181543_0_20260619_124038_image.png",
"20260622_181201_0_20260619_124038_image.png",
"20260622_175746_7_20260622_175551_image.png",
"20260622_175729_6_20260622_175551_image.png",
"20260622_175645_3_20260622_175551_image.png",
"20260622_175633_2_20260622_175551_image.png",
"20260622_175622_1_20260622_175551_image.png",
"20260622_175609_0_20260622_175551_image.png",
"20260622_175551_image.png",
"20260622_174030_6_20260622_173855_image.png",
"20260622_174013_5_20260622_173855_image.png",
"20260622_173956_4_20260622_173855_image.png",
"20260622_173940_3_20260622_173855_image.png",
"20260622_173928_2_20260622_173855_image.png",
"20260622_173917_1_20260622_173855_image.png",
"20260622_173906_0_20260622_173855_image.png",
"20260622_173855_image.png",
"20260622_163152_7_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163139_6_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163126_5_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163111_4_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163055_3_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163036_2_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163017_1_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_163004_0_20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_162948_Screenshot_From_2026-06-22_15-55-43.png",
"20260622_162939_7_20260622_162727_image.png",
"20260622_162925_6_20260622_162727_image.png",
"20260622_162910_5_20260622_162727_image.png",
"20260622_162833_3_20260622_162727_image.png",
"20260622_162816_2_20260622_162727_image.png",
"20260622_162759_1_20260622_162727_image.png",
"20260622_162744_0_20260622_162727_image.png",
"20260622_162727_image.png",
"20260622_162501_6_20260622_162338_image.png",
"20260622_162449_5_20260622_162338_image.png",
"20260622_162438_4_20260622_162338_image.png",
"20260622_162413_2_20260622_162338_image.png",
"20260622_162402_1_20260622_162338_image.png",
"20260622_162351_0_20260622_162338_image.png",
"20260622_162338_image.png",
"20260622_101342_5_20260622_101056_image.png",
"20260622_104936_image.nobg.png",
"20260622_134207_dup_20260622_104936_image.nobg.png",
"20260622_152438_image.png",
"20260622_152419_image.png",
"20260622_152336_image.png",
"20260622_152017_image.png",
"20260622_151959_image.png",
"20260622_151946_image.png",
"20260622_151320_2_20260622_101128_image.nobg.png",
"20260622_151308_2_20260622_101128_image.nobg.png",
"20260622_151235_2_20260622_101128_image.nobg.png",
"20260622_151224_2_20260622_101128_image.nobg.png",
"20260622_151213_2_20260622_101128_image.nobg.png",
"20260622_150929_2_20260622_101128_image.png",
"20260622_150907_2_20260622_101128_image.png",
"20260622_150855_2_20260622_101128_image.png",
"20260622_150831_2_20260622_101128_image.png",
"20260622_150820_2_20260622_101128_image.png",
"20260622_150637_2_20260622_101128_image.png",
"20260622_150603_2_20260622_101128_image.png",
"20260622_150546_2_20260622_101128_image.png",
"20260622_150529_2_20260622_101128_image.png",
"20260622_150513_2_20260622_101128_image.png",
"20260622_150500_2_20260622_101128_image.png",
"20260622_150443_2_20260622_101128_image.png",
"20260622_150426_2_20260622_101128_image.png",
"20260622_150409_2_20260622_101128_image.png",
"20260622_145046_2_20260622_101128_image.png",
"20260622_145026_2_20260622_101128_image.png",
"20260622_145007_2_20260622_101128_image.png",
"20260622_144949_2_20260622_101128_image.png",
"20260622_144931_2_20260622_101128_image.png",
"20260622_144857_2_20260622_101128_image.png",
"20260622_144659_1_20260622_101128_image.png",
"20260622_144641_1_20260622_101128_image.png",
"20260622_144623_1_20260622_101128_image.png",
"20260622_144605_1_20260622_101128_image.png",
"20260622_144529_1_20260622_101128_image.png",
"20260622_144512_1_20260622_101128_image.png",
"20260622_144436_1_20260622_101128_image.png",
"20260622_144217_2_20260622_101128_image.png",
"20260622_144202_2_20260622_101128_image.png",
"20260622_144145_2_20260622_101128_image.png",
"20260622_144128_2_20260622_101128_image.png",
"20260622_144025_2_20260622_101128_image.png",
"20260622_143657_1_20260622_101128_image.png",
"20260622_143634_1_20260622_101128_image.png",
"20260622_143623_1_20260622_101128_image.png",
"20260622_143612_1_20260622_101128_image.png",
"20260622_143601_1_20260622_101128_image.png",
"20260622_143549_1_20260622_101128_image.png",
"20260622_143538_1_20260622_101128_image.png",
"20260622_143439_1_20260622_101128_image.png",
"20260622_143422_1_20260622_101128_image.png",
"20260622_142649_2_20260622_101128_image.png",
"20260622_142457_1_20260622_101128_image.png",
"20260622_142320_2_20260622_101128_image.nobg.png",
"20260622_141903_2_20260622_101128_image.nobg.png",
"20260622_141751_2_20260622_101128_image.nobg.png",
"20260622_141626_1_20260622_101128_image.png",
"20260622_104622_image.nobg.png",
"20260622_130905_fs_SectionOriginal_image.png.mp4",
"20260622_130633_2_20260621_115215_image.png",
"20260622_130622_2_20260621_115215_image.png",
"20260622_130611_2_20260621_115215_image.png",
"20260622_130600_2_20260621_115215_image.png",
"20260622_130548_2_20260621_115215_image.png",
"20260622_130537_2_20260621_115215_image.png",
"20260622_130526_2_20260621_115215_image.png",
"20260622_130514_2_20260621_115215_image.png",
"20260622_130503_2_20260621_115215_image.png",
"20260622_130452_2_20260621_115215_image.png",
"20260622_130440_2_20260621_115215_image.png",
"20260622_130429_2_20260621_115215_image.png",
"20260622_130418_2_20260621_115215_image.png",
"20260622_130406_2_20260621_115215_image.png",
"20260622_130355_2_20260621_115215_image.png",
"20260622_130344_2_20260621_115215_image.png",
"20260622_130333_2_20260621_115215_image.png",
"20260622_130321_2_20260621_115215_image.png",
"20260622_130310_2_20260621_115215_image.png",
"20260622_130259_2_20260621_115215_image.png",
"20260622_130248_2_20260621_115215_image.png",
"20260622_130237_2_20260621_115215_image.png",
"20260622_130225_2_20260621_115215_image.png",
"20260622_130214_2_20260621_115215_image.png",
"20260622_130204_2_20260621_115215_image.png",
"20260622_130153_2_20260621_115215_image.png",
"20260622_130101_2_20260621_115215_image.png",
"20260622_130049_2_20260621_115215_image.png",
"20260622_130038_2_20260621_115215_image.png",
"20260622_130015_2_20260621_115215_image.png",
"20260622_130004_2_20260621_115215_image.png",
"20260622_125930_2_20260621_115215_image.png",
"20260622_125908_2_20260621_115215_image.png",
"20260622_125104_fs_BathRoom_247s-265s_image.png.mp4",
"20260622_124127_fs_mp4_sd_973s-1077s_image.png.mp4",
"20260622_124014_image.png",
"20260622_123629_image.png",
"20260622_122709_fs_mp4_sd_973s-1077s_image.png.mp4",
"20260622_115557_image.png",
"20260622_114540_image.png",
"20260622_113509_image.png",
"20260622_113309_image.png",
"20260622_113025_image.png",
"20260622_112640_image.png",
"20260622_112144_image.png",
"20260622_111959_image.png",
"20260622_111906_image.png",
"20260622_110937_image.png",
"20260622_110838_image.png",
"20260622_110821_image.png",
"20260622_110713_image.png",
"20260622_110605_image.png",
"20260622_110150_image.png",
"20260622_110026_image.png",
"20260622_105953_image.png",
"20260622_105837_6_20260622_091731_image.png",
"20260622_105627_image.png",
"20260622_101056_image.nobg.png",
"20260622_104207_2_20260622_101128_image.nobg.png",
"20260622_104155_2_20260622_101128_image.nobg.png",
"20260622_104142_2_20260622_101128_image.nobg.png",
"20260622_104130_2_20260622_101128_image.nobg.png",
"20260622_104039_2_20260622_101128_image.png",
"20260622_101252_2_20260622_101128_image.nobg.png",
"20260622_103029_0_20260622_101128_image.png",
"20260622_102856_0_20260622_101128_image.png",
"20260622_101554_8_20260622_101128_image.png",
"20260622_101537_7_20260622_101128_image.png",
"20260622_101521_8_20260622_101056_image.png",
"20260622_101504_6_20260622_101128_image.png",
"20260622_101432_5_20260622_101128_image.png",
"20260622_101415_6_20260622_101056_image.png",
"20260622_101359_4_20260622_101128_image.png",
"20260622_101252_2_20260622_101128_image.png",
"20260622_101219_1_20260622_101128_image.png",
"20260622_101147_0_20260622_101128_image.png",
"20260622_101128_image.png",
"20260622_101056_image.png",
"20260622_100921_image.png",
"20260622_100902_image.png",
"20260622_100842_image.png",
"20260622_100823_image.png",
"20260622_100700_image.png",
"20260622_100423_image.png",
"20260622_100314_image.png",
"20260622_100036_image.png",
"20260622_095947_image.png",
"20260622_095929_image.png",
"20260622_095832_image.png",
"20260622_093228_7_20260622_093058_image.png",
"20260622_093216_6_20260622_093058_image.png",
"20260622_093205_5_20260622_093058_image.png",
"20260622_093154_4_20260622_093058_image.png",
"20260622_093142_3_20260622_093058_image.png",
"20260622_093131_2_20260622_093058_image.png",
"20260622_093120_1_20260622_093058_image.png",
"20260622_093058_image.png",
"20260622_091851_6_20260622_091731_image.png",
"20260622_091839_5_20260622_091731_image.png",
"20260622_091828_4_20260622_091731_image.png",
"20260622_091817_3_20260622_091731_image.png",
"20260622_091744_0_20260622_091731_image.png",
"20260622_091731_image.png",
"20260622_054019_fs_badly_2_20260621_115215_image.png.mp4",
"20260622_053758_2_20260621_115215_image.png",
"20260622_053746_2_20260621_115215_image.png",
"20260622_053735_2_20260621_115215_image.png",
"20260622_053724_2_20260621_115215_image.png",
"20260622_053641_2_20260621_115215_image.png",
"20260622_053630_2_20260621_115215_image.png",
"20260622_053423_0_20260621_205151_image.png",
"20260622_053412_0_20260621_205151_image.png",
"20260622_053350_0_20260621_205151_image.png",
"20260622_053246_image.png",
"20260622_053235_image.png",
"20260622_053225_image.png",
"20260622_053054_image.png",
"20260622_052430_image.png",
"20260622_052419_image.png",
"20260622_052407_image.png",
"20260622_052345_image.png",
"20260622_052333_image.png",
"20260622_052322_image.png",
"20260622_052300_image.png",
"20260622_052220_image.png",
"20260622_051734_image.png",
"20260622_051711_image.png",
"20260622_051655_image.png",
"20260622_051644_image.png",
"20260622_050048_image.png",
"20260622_045636_4_20260621_205151_image.png",
"20260622_045623_4_20260621_205151_image.png",
"20260622_045558_4_20260621_205151_image.png",
"20260622_045545_4_20260621_205151_image.png",
"20260622_045413_image.png",
"20260622_045401_image.png",
"20260622_045136_image.png",
"20260622_042822_image.png",
"20260622_042801_image.png",
"20260622_042750_image.png",
"20260622_041503_image.png",
"20260622_041451_image.png",
"20260622_041438_image.png",
"20260622_041319_image.png",
"20260622_041306_image.png",
"20260622_041253_image.png",
"20260622_040911_b1.png",
"20260622_040727_b1.png",
"20260622_040702_b1.png",
"20260622_040636_b1.png",
"20260622_040347_b1.png",
"20260622_035945_b1.png",
"20260622_034529_b1.png",
"20260622_033840_b1.png",
"20260622_033829_b1.png",
"20260622_033806_b1.png",
"20260622_033744_b1.png",
"20260622_033733_b1.png",
"20260622_033722_b1.png",
"20260622_033700_b1.png",
"20260622_033649_b1.png",
"20260622_033638_b1.png",
"20260622_033628_b1.png",
"20260622_033617_b1.png",
"20260622_033134_b1.png",
"20260622_033123_b1.png",
"20260622_033112_b1.png",
"20260622_033101_b1.png",
"20260622_033050_b1.png",
"20260622_033039_b1.png",
"20260622_033029_b1.png",
"20260622_033018_b1.png",
"20260622_033007_b1.png",
"20260622_032956_b1.png",
"20260622_032223_b1.png",
"20260622_032212_b1.png",
"20260622_032201_b1.png",
"20260622_032150_b1.png",
"20260622_032140_b1.png",
"20260622_032118_b1.png",
"20260622_031725_b1.png",
"20260622_031708_b1.png",
"20260622_030845_b1.png",
"20260622_030832_b1.png",
"20260622_030754_b1.png",
"20260622_030728_b1.png",
"20260622_030715_b1.png",
"20260622_025308_b1.png",
"20260622_025234_b1.png",
"20260622_025128_b1.png",
"20260622_025116_b1.png",
"20260622_025105_b1.png",
"20260622_025055_b1.png",
"20260622_025044_b1.png",
"20260622_025033_b1.png",
"20260622_024933_b1.png",
"20260622_024740_b1.png",
"20260622_024730_b1.png",
"20260622_024719_b1.png",
"20260622_024708_b1.png",
"20260622_024600_b1.png",
"20260622_024437_b1.png",
"20260622_024426_b1.png",
"20260622_024311_b1.png",
"20260622_024159_b1.png",
"20260622_023750_b1.png",
"20260618_010638_b1.png",
"20260622_020521_dup_20260622_020223_image.nobg.png",
"20260622_021658_dup_20260621_212650_image.nobg.png",
"20260622_020521_dup_20260622_020223_image.png",
"20260622_020223_image.png",
"20260622_013636_image.nobg.png",
"20260622_015031_image.png",
"20260622_013636_image.png",
"20260622_013625_image.png",
"20260622_013614_image.png",
"20260622_005857_image.png",
"20260622_004346_fs_vacation_722s-775s_image.png.mp4",
"20260622_003713_fs_vacation_722s-775s_image.png.mp4",
"20260622_002215_fs_badly_1039s-1109s_1_20260619_202924_image.png.mp4",
"20260622_001127_fs_badly_949s-1038s_image.png.mp4",
"20260621_235251_fs_BathRoom_368s-386s_image.png.mp4",
"20260621_235129_fs_BathRoom_368s-386s_image.png.mp4",
"20260621_233236_fs_BathRoom_368s-386s_image.png.mp4",
"20260621_233034_fs_BathRoom_247s-265s_image.png.mp4",
"20260621_232901_fs_friend_260s-273s_image.png.mp4",
"20260621_232825_fs_friend_260s-273s_0_20260621_205151_image.png.mp4",
"20260621_232715_fs_friend_260s-273s_image.png.mp4",
"20260621_231051_image.png",
"20260621_231005_image.png",
"20260618_011539_20260617_133129_img_77.png",
"20260620_152017_image.png",
"20260618_013452_out4.png",
"20260620_025959_out4.png",
"20260622_021658_dup_20260621_212650_image.png",
"20260621_212650_image.png",
"20260621_210130_0_20260621_205151_image.png",
"20260621_210118_0_20260621_205151_image.png",
"20260621_210107_0_20260621_205151_image.png",
"20260621_205908_0_20260621_205151_image.png",
"20260621_205648_image.png",
"20260621_205356_7_20260621_205151_image.png",
"20260621_205338_6_20260621_205151_image.png",
"20260621_205321_5_20260621_205151_image.png",
"20260621_205304_4_20260621_205151_image.png",
"20260621_205247_3_20260621_205151_image.png",
"20260621_205230_2_20260621_205151_image.png",
"20260621_205213_1_20260621_205151_image.png",
"20260621_205202_0_20260621_205151_image.png",
"20260621_205151_image.png",
"20260621_203218_8_20260621_202941_image.png",
"20260621_203201_7_20260621_202941_image.png",
"20260621_203143_6_20260621_202941_image.png",
"20260621_203126_5_20260621_202941_image.png",
"20260621_203108_4_20260621_202941_image.png",
"20260621_203050_3_20260621_202941_image.png",
"20260621_203028_2_20260621_202941_image.png",
"20260621_203009_1_20260621_202941_image.png",
"20260621_202956_0_20260621_202941_image.png",
"20260621_202941_image.png",
"20260621_202759_8_20260621_202521_image.png",
"20260621_202743_7_20260621_202521_image.png",
"20260621_202726_6_20260621_202521_image.png",
"20260621_202704_5_20260621_202521_image.png",
"20260621_202630_3_20260621_202521_image.png",
"20260621_202615_2_20260621_202521_image.png",
"20260621_202602_1_20260621_202521_image.png",
"20260621_202548_0_20260621_202521_image.png",
"20260621_202521_image.png",
"20260621_115348_7_20260621_115215_image.png",
"20260621_115337_6_20260621_115215_image.png",
"20260621_115325_5_20260621_115215_image.png",
"20260621_115314_4_20260621_115215_image.png",
"20260621_115303_3_20260621_115215_image.png",
"20260621_115249_2_20260621_115215_image.png",
"20260621_115238_1_20260621_115215_image.png",
"20260621_115227_0_20260621_115215_image.png",
"20260621_115215_image.png",
"20260621_112614_sc_image.png",
"20260621_055418_sc_image.png",
"20260621_054553_sc_image.png",
"20260621_055114_sc_image.png",
"20260621_054746_sc_image.png",
"20260621_054610_sc_image.png",
"20260621_054243_sc_sc_sc_image.png",
"20260621_054214_sc_sc_image.png",
"20260621_054137_sc_image.png",
"20260621_054053_sc_image.png",
"20260621_053822_sc_image.png",
"20260621_053520_sc_image.png",
"20260621_053447_sc_sc_image.png",
"20260621_053345_sc_image.png",
"20260621_053228_sc_image.png",
"20260621_053131_sc_image.png",
"20260621_052951_image.png",
"20260621_052940_image.png",
"20260621_052930_image.png",
"20260621_052908_image.png",
"20260621_052621_image.png",
"20260621_052610_image.png",
"20260621_052600_image.png",
"20260621_052549_image.png",
"20260621_052538_image.png",
"20260621_052527_image.png",
"20260621_051626_sc_image.png",
"20260621_032154_image.png",
"20260621_045740_sc_image.png",
"20260621_045015_image.png",
"20260621_044941_image.png",
"20260621_044930_image.png",
"20260621_044908_image.png",
"20260621_044508_image.png",
"20260621_044446_image.png",
"20260621_044435_image.png",
"20260621_044048_image.png",
"20260621_043651_sc_sc_image.png",
"20260621_032916_image.png",
"20260621_040928_sc_image.png",
"20260621_040421_0_20260621_034735_image.png",
"20260621_040406_0_20260621_034735_image.png",
"20260621_040349_0_20260621_034735_image.png",
"20260621_040332_0_20260621_034735_image.png",
"20260621_040315_0_20260621_034735_image.png",
"20260621_040159_0_20260621_034735_image.png",
"20260621_034842_5_20260621_034735_image.png",
"20260621_034747_0_20260621_034735_image.png",
"20260621_034735_image.png",
"20260621_033325_image.png",
"20260621_033259_image.png",
"20260621_033234_image.png",
"20260621_033221_image.png",
"20260621_033208_image.png",
"20260621_033155_image.png",
"20260621_032903_image.png",
"20260621_032850_image.png",
"20260621_032836_image.png",
"20260621_032810_image.png",
"20260621_032709_image.png",
"20260621_032601_image.png",
"20260621_032218_image.png",
"20260621_032206_image.png",
"20260621_032143_image.png",
"20260621_032131_image.png",
"20260621_032119_image.png",
"20260621_032108_image.png",
"20260621_032056_image.png",
"20260621_032045_image.png",
"20260621_032034_image.png",
"20260621_031031_image.png",
"20260620_162107_fs_friend_image.png.mp4",
"20260620_155749_fs_friend_260s-273s_6_20260620_150941_image.png.mp4",
"20260620_155149_fs_friend_200s-220s_1_20260620_151425_image.png.mp4",
"20260620_154959_8_20260620_154810_image.png",
"20260620_154948_7_20260620_154810_image.png",
"20260620_154936_6_20260620_154810_image.png",
"20260620_154925_5_20260620_154810_image.png",
"20260620_154913_4_20260620_154810_image.png",
"20260620_154901_3_20260620_154810_image.png",
"20260620_154849_2_20260620_154810_image.png",
"20260620_154839_1_20260620_154810_image.png",
"20260620_154828_0_20260620_154810_image.png",
"20260620_154810_image.png",
"20260620_152926_1_20260620_151425_image.png",
"20260620_152838_image.png",
"20260620_151954_6_20260620_151835_image.png",
"20260620_151908_2_20260620_151835_image.png",
"20260620_151858_1_20260620_151835_image.png",
"20260620_151847_0_20260620_151835_image.png",
"20260620_151835_image.png",
"20260620_151628_1_20260620_151555_image.png",
"20260620_151555_image.png",
"20260620_151543_6_20260620_151425_image.png",
"20260620_151532_5_20260620_151425_image.png",
"20260620_151521_4_20260620_151425_image.png",
"20260620_151458_2_20260620_151425_image.png",
"20260620_151447_1_20260620_151425_image.png",
"20260620_151436_0_20260620_151425_image.png",
"20260620_151425_image.png",
"20260620_151323_6_20260620_150941_image.png",
"20260620_151249_3_20260620_150941_image.png",
"20260620_151223_1_20260620_150941_image.png",
"20260620_151209_0_20260620_150941_image.png",
"20260620_150941_image.png",
"20260620_053812_image.png",
"20260620_053801_image.png",
"20260620_053749_image.png",
"20260620_053738_image.png",
"20260620_053727_image.png",
"20260620_053716_image.png",
"20260620_053705_image.png",
"20260620_053641_image.png",
"20260620_053630_image.png",
"20260620_045040_fs_BathRoom_image.png.mp4",
"20260620_044811_fs_stair_2_20260619_202924_image.png.mp4",
"20260618_015102_20260616_022543_img_50.png",
"20260620_034637_fs_BathRoom_image.png.mp4",
"20260620_032550_20160903_200935.jpg",
"20260620_031338_20150913_211324.jpg",
"20260620_031211_20150913_211324.jpg",
"20260620_030933_20150913_211324.jpg",
"20260620_030839_20150913_211324.jpg",
"20260620_030644_4_20260618_125217_image.png",
"20260620_030633_4_20260618_125217_image.png",
"20260620_025855_b1.png",
"20260620_021352_2_20260618_172500_image.png",
"20260620_021330_2_20260618_172500_image.png",
"20260620_021319_2_20260618_172500_image.png",
"20260620_021308_2_20260618_172500_image.png",
"20260620_021258_2_20260618_172500_image.png",
"20260620_021247_2_20260618_172500_image.png",
"20260620_021226_2_20260618_172500_image.png",
"20260620_021215_2_20260618_172500_image.png",
"20260620_020431_2_20260618_172500_image.png",
"20260620_020408_2_20260618_172500_image.png",
"20260620_020357_2_20260618_172500_image.png",
"20260620_020346_2_20260618_172500_image.png",
"20260620_020335_2_20260618_172500_image.png",
"20260620_020324_2_20260618_172500_image.png",
"20260620_020313_2_20260618_172500_image.png",
"20260620_020302_2_20260618_172500_image.png",
"20260620_020251_2_20260618_172500_image.png",
"20260620_014953_image.png",
"20260620_014646_image.png",
"20260620_014349_image.png",
"20260620_014239_img_77.png",
"20260620_003901_2_20260619_175210_image.png",
"20260620_003844_2_20260619_175210_image.png",
"20260620_003828_2_20260619_175210_image.png",
"20260620_003708_2_20260619_175210_image.png",
"20260620_002715_img_38.png",
"20260620_002558_img_38.png",
"20260620_001342_1_20260619_202924_image.png",
"20260620_001328_1_20260619_202924_image.png",
"20260620_001230_1_20260619_202924_image.png",
"20260620_001215_1_20260619_202924_image.png",
"20260620_001202_1_20260619_202924_image.png",
"20260620_001145_1_20260619_202924_image.png",
"20260620_001129_1_20260619_202924_image.png",
"20260620_001100_1_20260619_202924_image.png",
"20260620_001048_1_20260619_202924_image.png",
"20260620_001032_1_20260619_202924_image.png",
"20260620_001003_1_20260619_202924_image.png",
"20260620_000453_1_20260619_202924_image.png",
"20260620_000437_1_20260619_202924_image.png",
"20260620_000404_1_20260619_202924_image.png",
"20260620_000353_1_20260619_202924_image.png",
"20260620_000330_1_20260619_202924_image.png",
"20260620_000319_1_20260619_202924_image.png",
"20260620_000308_1_20260619_202924_image.png",
"20260620_000257_1_20260619_202924_image.png",
"20260619_235919_1_20260619_202924_image.png",
"20260619_235909_1_20260619_202924_image.png",
"20260619_235858_1_20260619_202924_image.png",
"20260619_235847_1_20260619_202924_image.png",
"20260619_235614_1_20260619_202924_image.png",
"20260619_235557_1_20260619_202924_image.png",
"20260619_235541_1_20260619_202924_image.png",
"20260619_235514_1_20260619_202924_image.png",
"20260619_202956_1_20260619_202924_image.png",
"20260619_233813_img_38.png",
"20260619_225553_img_42.png",
"20260619_225523_img_84.png",
"20260619_225509_img_60.png",
"20260619_225409_img_59.png",
"20260619_225314_img_4.png",
"20260619_225244_img_86.png",
"20260619_225229_imgxxx.png",
"20260619_225215_img_15.png",
"20260619_225146_img_83.png",
"20260619_224934_img_38.png",
"20260619_224919_img_51.png",
"20260619_224851_img_36.png",
"20260619_224737_img_14.png",
"20260619_224557_img_81.png",
"20260619_224542_img_35.png",
"20260619_224133_img_87.png",
"20260619_224007_img_31.png",
"20260619_223856_img_37.png",
"20260619_223812_img_57.png",
"20260619_223757_img_82.png",
"20260619_223725_20160903_200935.jpg",
"20260619_223712_img_50.png",
"20260619_223643_img_48.png",
"20260619_223529_img_52.png",
"20260619_223512_20150913_211324.jpg",
"20260619_223441_img_77.png",
"20260619_223422_img_18.png",
"20260619_223330_img_73.png",
"20260619_223309_img_72.png",
"20260619_223249_img_80.png",
"20260619_223218_img_58.png",
"20260619_223101_img_41.png",
"20260619_223051_img_44.png",
"20260619_223040_img_78.png",
"20260619_205500_others.jpeg",
"20260619_205758_others.jpeg",
"20260619_205511_others.jpeg",
"20260619_205310_others.jpeg",
"20260618_004407_20260616_002456_test123.jpeg",
"20260619_204052_others.jpeg",
"20260619_204041_others.jpeg",
"20260619_204019_others.jpeg",
"20260619_204008_others.jpeg",
"20260619_203945_others.jpeg",
"20260619_203934_others.jpeg",
"20260619_203923_others.jpeg",
"20260619_203912_others.jpeg",
"20260619_203901_others.jpeg",
"20260619_203851_others.jpeg",
"20260619_203840_others.jpeg",
"20260619_203255_8_20260619_202924_image.png",
"20260619_203240_7_20260619_202924_image.png",
"20260619_203207_6_20260619_202924_image.png",
"20260619_203152_7_20260619_202903_image.png",
"20260619_203135_5_20260619_202924_image.png",
"20260619_203119_6_20260619_202903_image.png",
"20260619_203103_4_20260619_202924_image.png",
"20260619_203051_5_20260619_202903_image.png",
"20260619_203040_3_20260619_202924_image.png",
"20260619_203029_4_20260619_202903_image.png",
"20260619_203017_2_20260619_202924_image.png",
"20260619_203009_3_20260619_202903_image.png",
"20260619_202946_2_20260619_202903_image.png",
"20260619_202935_0_20260619_202924_image.png",
"20260619_202925_1_20260619_202903_image.png",
"20260619_202924_image.png",
"20260619_202914_0_20260619_202903_image.png",
"20260619_202903_image.png",
"20260619_200022_others.jpeg",
"20260619_195113_others.jpeg",
"20260619_195102_others.jpeg",
"20260619_195040_others.jpeg",
"20260619_195030_others.jpeg",
"20260619_194512_test123.jpeg",
"20260619_194429_test123.jpeg",
"20260619_194418_test123.jpeg",
"20260619_190224_7_20260619_190056_image.png",
"20260619_190213_6_20260619_190056_image.png",
"20260619_190201_5_20260619_190056_image.png",
"20260619_190150_4_20260619_190056_image.png",
"20260619_190139_3_20260619_190056_image.png",
"20260619_190128_2_20260619_190056_image.png",
"20260619_190117_1_20260619_190056_image.png",
"20260619_190106_0_20260619_190056_image.png",
"20260619_190056_image.png",
"20260619_184513_8_20260619_184329_image.png",
"20260619_184501_7_20260619_184329_image.png",
"20260619_184449_6_20260619_184329_image.png",
"20260619_184438_5_20260619_184329_image.png",
"20260619_184426_4_20260619_184329_image.png",
"20260619_184414_3_20260619_184329_image.png",
"20260619_184403_2_20260619_184329_image.png",
"20260619_184351_1_20260619_184329_image.png",
"20260619_184341_0_20260619_184329_image.png",
"20260619_184329_image.png",
"20260619_184236_6_20260619_184116_image.png",
"20260619_184139_1_20260619_184116_image.png",
"20260619_184128_0_20260619_184116_image.png",
"20260619_184116_image.png",
"20260619_181536_8_20260619_181308_img.png",
"20260619_181522_7_20260619_181308_img.png",
"20260619_181504_6_20260619_181308_img.png",
"20260619_181448_5_20260619_181308_img.png",
"20260619_181431_4_20260619_181308_img.png",
"20260619_181414_3_20260619_181308_img.png",
"20260619_181357_2_20260619_181308_img.png",
"20260619_181340_1_20260619_181308_img.png",
"20260619_181324_0_20260619_181308_img.png",
"20260619_181308_img.png",
"20260619_175426_7_20260619_175210_image.png",
"20260619_175409_6_20260619_175210_image.png",
"20260619_175335_4_20260619_175210_image.png",
"20260619_175317_3_20260619_175210_image.png",
"20260619_175300_2_20260619_175210_image.png",
"20260619_175243_1_20260619_175210_image.png",
"20260619_175227_0_20260619_175210_image.png",
"20260619_175210_image.png",
"20260616_002456_test123.jpeg",
"20260619_130342_others.jpeg",
"20260619_130358_others.jpeg",
"20260619_130415_others.jpeg",
"20260619_130606_others.jpeg",
"20260615_155354_others.jpeg",
"20260619_130726_others.jpeg",
"20260619_130742_others.jpeg",
"20260618_015052_20260618_004407_20260616_002456_test123.jpeg",
"20260619_172850_8_20260619_172708_image.png",
"20260619_172839_7_20260619_172708_image.png",
"20260619_172827_6_20260619_172708_image.png",
"20260619_172816_5_20260619_172708_image.png",
"20260619_172804_4_20260619_172708_image.png",
"20260619_172753_3_20260619_172708_image.png",
"20260619_172741_2_20260619_172708_image.png",
"20260619_172730_1_20260619_172708_image.png",
"20260619_172719_0_20260619_172708_image.png",
"20260619_172708_image.png",
"20260619_133311_8_20260619_133054_image.png",
"20260619_133300_7_20260619_133054_image.png",
"20260619_133248_6_20260619_133054_image.png",
"20260619_133237_5_20260619_133054_image.png",
"20260619_133226_4_20260619_133054_image.png",
"20260619_133214_3_20260619_133054_image.png",
"20260619_133203_8_20260619_132948_image.png",
"20260619_133152_2_20260619_133054_image.png",
"20260619_133141_7_20260619_132948_image.png",
"20260619_133128_1_20260619_133054_image.png",
"20260619_133117_6_20260619_132948_image.png",
"20260619_133107_0_20260619_133054_image.png",
"20260619_133056_5_20260619_132948_image.png",
"20260619_133054_image.png",
"20260619_133044_4_20260619_132948_image.png",
"20260619_133032_3_20260619_132948_image.png",
"20260619_133021_2_20260619_132948_image.png",
"20260619_133010_1_20260619_132948_image.png",
"20260619_132959_0_20260619_132948_image.png",
"20260619_132948_image.png",
"20260619_130712_others.jpeg",
"20260619_130549_others.jpeg",
"20260619_130043_mr_1_20260619_124038_image.png",
"20260619_125654_mr_mr_7_20260619_124038_image.png",
"20260619_125445_mr_7_20260619_124038_image.png",
"20260619_125434_mr_mr_1_20260619_040135_image.png",
"20260619_124456_6_20260619_124038_image.png",
"20260619_124435_5_20260619_124038_image.png",
"20260619_124351_4_20260619_123958_image.png",
"20260619_124333_3_20260619_124038_image.png",
"20260619_124226_1_20260619_124038_image.png",
"20260619_124154_0_20260619_124038_image.png",
"20260619_124139_0_20260619_123958_image.png",
"20260619_124038_image.png",
"20260619_123958_image.png",
"20260619_052514_mr_1_20260618_052526_image.png",
"20260619_052351_mr_mr_mr_mr_1_20260619_040135_image.png",
"20260619_052235_mr_1_20260619_040135_image.png",
"20260619_052030_mr_mr_mr_1_20260619_040135_image.png",
"20260619_051905_mr_mr_1_20260619_040135_image.png",
"20260619_051326_mr_1_20260619_040135_image.png",
"20260619_051245_1_20260619_040135_image.png",
"20260619_051226_mr_1_20260619_040135_image.png",
"20260619_050945_image.png",
"20260619_050207_1_20260619_040135_image.png",
"20260619_050114_1_20260619_040135_image.png",
"20260619_045946_1_20260619_040135_image.png",
"20260619_043856_1_20260619_040135_image.png",
"20260619_043845_1_20260619_040135_image.png",
"20260619_043740_1_20260619_040135_image.png",
"20260619_041924_8_20260619_041612_image.png",
"20260619_041913_1_20260619_040135_image.png",
"20260619_041902_7_20260619_041612_image.png",
"20260619_041850_1_20260619_040135_image.png",
"20260619_041839_6_20260619_041612_image.png",
"20260619_041829_1_20260619_040135_image.png",
"20260619_041818_5_20260619_041612_image.png",
"20260619_041806_1_20260619_040135_image.png",
"20260619_041756_4_20260619_041612_image.png",
"20260619_041744_1_20260619_040135_image.png",
"20260619_041734_3_20260619_041612_image.png",
"20260619_041722_1_20260619_040135_image.png",
"20260619_041711_2_20260619_041612_image.png",
"20260619_041701_1_20260619_040135_image.png",
"20260619_041639_1_20260619_040135_image.png",
"20260619_041628_0_20260619_041612_image.png",
"20260619_041618_1_20260619_040135_image.png",
"20260619_041612_image.png",
"20260619_041555_1_20260619_040135_image.png",
"20260619_041544_1_20260619_040135_image.png",
"20260619_041533_1_20260619_040135_image.png",
"20260619_041511_1_20260619_040135_image.png",
"20260619_041500_1_20260619_040135_image.png",
"20260619_041449_1_20260619_040135_image.png",
"20260619_041427_1_20260619_040135_image.png",
"20260619_041417_1_20260619_040135_image.png",
"20260619_041406_1_20260619_040135_image.png",
"20260619_041355_1_20260619_040135_image.png",
"20260619_041334_1_20260619_040135_image.png",
"20260619_041323_1_20260619_040135_image.png",
"20260619_041041_1_20260619_040135_image.png",
"20260619_041030_1_20260619_040135_image.png",
"20260619_041020_1_20260619_040135_image.png",
"20260619_040147_0_20260619_040135_image.png",
"20260619_040209_2_20260619_040135_image.png",
"20260619_040158_1_20260619_040135_image.png",
"20260619_040319_8_20260619_040135_image.png",
"20260619_040307_7_20260619_040135_image.png",
"20260619_040255_6_20260619_040135_image.png",
"20260619_040244_5_20260619_040135_image.png",
"20260619_040233_4_20260619_040135_image.png",
"20260619_040221_3_20260619_040135_image.png",
"20260619_040135_image.png",
"20260619_035006_2_20260618_053458_image.png",
"20260619_034656_2_20260618_053458_image.png",
"20260619_034055_2_20260618_053458_image.png",
"20260619_034044_2_20260618_053458_image.png",
"20260619_034033_2_20260618_053458_image.png",
"20260619_034011_2_20260618_053458_image.png",
"20260619_034001_2_20260618_053458_image.png",
"20260619_033950_2_20260618_053458_image.png",
"20260619_033939_2_20260618_053458_image.png",
"20260619_033804_2_20260618_053458_image.png",
"20260619_033754_2_20260618_053458_image.png",
"20260619_033743_2_20260618_053458_image.png",
"20260619_033732_2_20260618_053458_image.png",
"20260619_033721_2_20260618_053458_image.png",
"20260619_032726_2_20260618_053458_image.png",
"20260619_032715_2_20260618_053458_image.png",
"20260619_032704_2_20260618_053458_image.png",
"20260619_032257_0_20260618_181600_image.png",
"20260619_031916_image.png",
"20260619_024726_jb1.png",
"20260619_024715_jb1.png",
"20260619_024704_jb1.png",
"20260619_024327_jb1.png",
"20260618_010649_20260615_150340_test.png",
"20260618_011622_jb1.png",
"20260619_024118_0_20260618_181600_image.png",
"20260619_024056_0_20260618_181600_image.png",
"20260619_023250_image.png",
"20260619_022156_3_20260618_181600_image.png",
"20260619_022028_2_20260618_174613_image.png",
"20260619_021829_2_20260618_053458_image.png",
"20260619_021736_2_20260618_053458_image.png",
"20260619_020115_2_20260618_053458_image.png",
"20260619_015517_2_20260618_053458_image.png",
"20260619_015140_2_20260618_053458_image.png",
"20260619_014803_2_20260618_053458_image.png",
"20260619_012837_2_20260618_053458_image.png",
"20260619_004149_3_20260618_174613_image.png",
"20260619_003516_0_20260618_174613_image.png",
"20260619_002329_2_20260618_053458_image.png",
"20260619_001659_4_20260618_173728_image.png",
"20260618_234918_3_20260618_173728_image.png",
"20260618_231151_3_20260618_173728_image.png",
"20260618_231042_1_20260618_173728_image.png",
"20260618_230950_3_20260618_173728_image.png",
"20260618_225918_7_20260618_173728_image.png",
"20260618_225236_3_20260618_173728_image.png",
"20260618_223557_2_20260618_223501_image.png",
"20260618_223541_1_20260618_223501_image.png",
"20260618_223525_0_20260618_223501_image.png",
"20260618_223501_image.png",
"20260618_220918_2_20260618_053458_image.png",
"20260618_220907_2_20260618_053458_image.png",
"20260618_220856_2_20260618_053458_image.png",
"20260618_182944_7_20260618_182810_image.png",
"20260618_182932_6_20260618_182810_image.png",
"20260618_182920_5_20260618_182810_image.png",
"20260618_182909_4_20260618_182810_image.png",
"20260618_182857_3_20260618_182810_image.png",
"20260618_182842_2_20260618_182810_image.png",
"20260618_182832_1_20260618_182810_image.png",
"20260618_182821_0_20260618_182810_image.png",
"20260618_182810_image.png",
"20260618_182304_8_20260618_182106_image.png",
"20260618_182247_7_20260618_182106_image.png",
"20260618_182230_6_20260618_182106_image.png",
"20260618_182216_5_20260618_182106_image.png",
"20260618_182204_4_20260618_182106_image.png",
"20260618_182153_3_20260618_182106_image.png",
"20260618_182141_2_20260618_182106_image.png",
"20260618_182130_1_20260618_182106_image.png",
"20260618_182117_0_20260618_182106_image.png",
"20260618_182106_image.png",
"20260618_182104_8_20260618_181848_image.png",
"20260618_182052_7_20260618_181848_image.png",
"20260618_182041_6_20260618_181848_image.png",
"20260618_182029_5_20260618_181848_image.png",
"20260618_182018_4_20260618_181848_image.png",
"20260618_182007_3_20260618_181848_image.png",
"20260618_181944_2_20260618_181848_image.png",
"20260618_181934_7_20260618_181715_image.png",
"20260618_181922_1_20260618_181848_image.png",
"20260618_181912_6_20260618_181715_image.png",
"20260618_181901_0_20260618_181848_image.png",
"20260618_181851_5_20260618_181715_image.png",
"20260618_181848_image.png",
"20260618_181839_4_20260618_181715_image.png",
"20260618_181827_3_20260618_181715_image.png",
"20260618_181815_2_20260618_181715_image.png",
"20260618_181804_8_20260618_181600_image.png",
"20260618_181752_1_20260618_181715_image.png",
"20260618_181742_7_20260618_181600_image.png",
"20260618_181730_0_20260618_181715_image.png",
"20260618_181719_6_20260618_181600_image.png",
"20260618_181715_image.png",
"20260618_181708_5_20260618_181600_image.png",
"20260618_181656_4_20260618_181600_image.png",
"20260618_181645_3_20260618_181600_image.png",
"20260618_181633_2_20260618_181600_image.png",
"20260618_181622_1_20260618_181600_image.png",
"20260618_181611_0_20260618_181600_image.png",
"20260618_181600_image.png",
"20260618_181109_7_20260618_180939_image.png",
"20260618_181057_6_20260618_180939_image.png",
"20260618_181045_5_20260618_180939_image.png",
"20260618_181034_4_20260618_180939_image.png",
"20260618_181023_3_20260618_180939_image.png",
"20260618_181011_2_20260618_180939_image.png",
"20260618_181000_1_20260618_180939_image.png",
"20260618_180950_0_20260618_180939_image.png",
"20260618_180939_image.png",
"20260618_174926_7_20260618_174613_image.png",
"20260618_174909_6_20260618_174613_image.png",
"20260618_174852_5_20260618_174613_image.png",
"20260618_174835_4_20260618_174613_image.png",
"20260618_174803_3_20260618_174613_image.png",
"20260618_174747_7_20260618_174516_image.png",
"20260618_174730_2_20260618_174613_image.png",
"20260618_174714_6_20260618_174516_image.png",
"20260618_174657_1_20260618_174613_image.png",
"20260618_174641_5_20260618_174516_image.png",
"20260618_174625_0_20260618_174613_image.png",
"20260618_174615_4_20260618_174516_image.png",
"20260618_174613_image.png",
"20260618_174603_3_20260618_174516_image.png",
"20260618_174540_1_20260618_174516_image.png",
"20260618_174529_0_20260618_174516_image.png",
"20260618_174516_image.png",
"20260618_173955_7_20260618_173728_image.png",
"20260618_173941_6_20260618_173728_image.png",
"20260618_173923_5_20260618_173728_image.png",
"20260618_173906_4_20260618_173728_image.png",
"20260618_173848_3_20260618_173728_image.png",
"20260618_173828_2_20260618_173728_image.png",
"20260618_173808_1_20260618_173728_image.png",
"20260618_173747_0_20260618_173728_image.png",
"20260618_173728_image.png",
"20260618_172641_8_20260618_172500_image.png",
"20260618_172630_7_20260618_172500_image.png",
"20260618_172618_6_20260618_172500_image.png",
"20260618_172607_5_20260618_172500_image.png",
"20260618_172555_4_20260618_172500_image.png",
"20260618_172544_3_20260618_172500_image.png",
"20260618_172532_2_20260618_172500_image.png",
"20260618_172521_1_20260618_172500_image.png",
"20260618_172510_0_20260618_172500_image.png",
"20260618_172500_image.png",
"20260618_172015_8_20260618_171448_image.png",
"20260618_171956_6_20260618_171628_image.png",
"20260618_171939_7_20260618_171448_image.png",
"20260618_171923_5_20260618_171628_image.png",
"20260618_171906_6_20260618_171448_image.png",
"20260618_171851_4_20260618_171628_image.png",
"20260618_171834_5_20260618_171448_image.png",
"20260618_171819_3_20260618_171628_image.png",
"20260618_171802_4_20260618_171448_image.png",
"20260618_171746_2_20260618_171628_image.png",
"20260618_171732_3_20260618_171448_image.png",
"20260618_171717_1_20260618_171628_image.png",
"20260618_171706_2_20260618_171448_image.png",
"20260618_171655_0_20260618_171628_image.png",
"20260618_171639_1_20260618_171448_image.png",
"20260618_171628_image.png",
"20260618_171624_0_20260618_171448_image.png",
"20260618_171448_image.png",
"20260618_134532_7_20260618_134348_image.png",
"20260618_134520_6_20260618_134348_image.png",
"20260618_134508_5_20260618_134348_image.png",
"20260618_134454_4_20260618_134348_image.png",
"20260618_134440_3_20260618_134348_image.png",
"20260618_134424_2_20260618_134348_image.png",
"20260618_134409_1_20260618_134348_image.png",
"20260618_134359_0_20260618_134348_image.png",
"20260618_134348_image.png",
"20260618_133922_7_20260618_133729_image.png",
"20260618_133910_6_20260618_133729_image.png",
"20260618_133858_5_20260618_133729_image.png",
"20260618_133847_4_20260618_133729_image.png",
"20260618_133835_3_20260618_133729_image.png",
"20260618_133822_2_20260618_133729_image.png",
"20260618_133811_1_20260618_133729_image.png",
"20260618_133800_0_20260618_133729_image.png",
"20260618_133729_image.png",
"20260618_133023_10_20260618_132718_image.png",
"20260618_133011_9_20260618_132718_image.png",
"20260618_132956_8_20260618_132718_image.png",
"20260618_132941_7_20260618_132718_image.png",
"20260618_132923_6_20260618_132718_image.png",
"20260618_132906_5_20260618_132718_image.png",
"20260618_132848_4_20260618_132718_image.png",
"20260618_132831_3_20260618_132718_image.png",
"20260618_132817_2_20260618_132718_image.png",
"20260618_132806_1_20260618_132718_image.png",
"20260618_132755_0_20260618_132718_image.png",
"20260618_132718_image.png",
"20260618_132450_8_20260618_132215_image.png",
"20260618_132432_7_20260618_132215_image.png",
"20260618_132414_6_20260618_132215_image.png",
"20260618_132357_5_20260618_132215_image.png",
"20260618_132340_4_20260618_132215_image.png",
"20260618_132323_3_20260618_132215_image.png",
"20260618_132305_2_20260618_132215_image.png",
"20260618_132248_1_20260618_132215_image.png",
"20260618_132232_0_20260618_132215_image.png",
"20260618_132215_image.png",
"20260618_132152_8_20260618_131909_image.png",
"20260618_132134_7_20260618_131909_image.png",
"20260618_132114_6_20260618_131909_image.png",
"20260618_132054_5_20260618_131909_image.png",
"20260618_132035_4_20260618_131909_image.png",
"20260618_132017_3_20260618_131909_image.png",
"20260618_131959_2_20260618_131909_image.png",
"20260618_131943_1_20260618_131909_image.png",
"20260618_131926_0_20260618_131909_image.png",
"20260618_131909_image.png",
"20260618_131811_7_20260618_131624_image.png",
"20260618_131759_6_20260618_131624_image.png",
"20260618_131747_5_20260618_131624_image.png",
"20260618_131736_4_20260618_131624_image.png",
"20260618_131725_3_20260618_131624_image.png",
"20260618_131713_2_20260618_131624_image.png",
"20260618_131702_1_20260618_131624_image.png",
"20260618_131651_0_20260618_131624_image.png",
"20260618_131624_image.png",
"20260618_125928_8_20260618_125748_image.png",
"20260618_125917_7_20260618_125748_image.png",
"20260618_125905_6_20260618_125748_image.png",
"20260618_125854_5_20260618_125748_image.png",
"20260618_125842_4_20260618_125748_image.png",
"20260618_125831_3_20260618_125748_image.png",
"20260618_125819_2_20260618_125748_image.png",
"20260618_125809_1_20260618_125748_image.png",
"20260618_125758_0_20260618_125748_image.png",
"20260618_125748_image.png",
"20260618_125358_5_20260618_125217_image.png",
"20260618_125340_4_20260618_125217_image.png",
"20260618_125323_3_20260618_125217_image.png",
"20260618_125307_2_20260618_125217_image.png",
"20260618_125250_1_20260618_125217_image.png",
"20260618_125233_0_20260618_125217_image.png",
"20260618_125217_image.png",
"20260618_124424_5_20260618_124203_image.png",
"20260618_124412_4_20260618_124203_image.png",
"20260618_124355_3_20260618_124203_image.png",
"20260618_124334_2_20260618_124203_image.png",
"20260618_124316_1_20260618_124203_image.png",
"20260618_124256_0_20260618_124203_image.png",
"20260618_124203_image.png",
"20260618_123244_6_20260618_122931_image.png",
"20260618_123227_5_20260618_122931_image.png",
"20260618_123210_4_20260618_122931_image.png",
"20260618_123153_3_20260618_122931_image.png",
"20260618_123132_2_20260618_122931_image.png",
"20260618_123104_0_20260618_122931_image.png",
"20260618_122931_image.png",
"20260618_054411_8_20260618_054230_image.png",
"20260618_054400_7_20260618_054230_image.png",
"20260618_054348_6_20260618_054230_image.png",
"20260618_054337_5_20260618_054230_image.png",
"20260618_054325_4_20260618_054230_image.png",
"20260618_054314_3_20260618_054230_image.png",
"20260618_054303_2_20260618_054230_image.png",
"20260618_054252_1_20260618_054230_image.png",
"20260618_054241_0_20260618_054230_image.png",
"20260618_054230_image.png",
"20260618_053920_6_20260618_053802_image.png",
"20260618_053909_5_20260618_053802_image.png",
"20260618_053857_4_20260618_053802_image.png",
"20260618_053846_3_20260618_053802_image.png",
"20260618_053834_2_20260618_053802_image.png",
"20260618_053823_1_20260618_053802_image.png",
"20260618_053812_0_20260618_053802_image.png",
"20260618_053802_image.png",
"20260618_053627_7_20260618_053458_image.png",
"20260618_053615_6_20260618_053458_image.png",
"20260618_053604_5_20260618_053458_image.png",
"20260618_053553_4_20260618_053458_image.png",
"20260618_053530_2_20260618_053458_image.png",
"20260618_053519_1_20260618_053458_image.png",
"20260618_053508_0_20260618_053458_image.png",
"20260618_053458_image.png",
"20260618_052708_8_20260618_052526_image.png",
"20260618_052657_7_20260618_052526_image.png",
"20260618_052645_6_20260618_052526_image.png",
"20260618_052634_5_20260618_052526_image.png",
"20260618_052622_4_20260618_052526_image.png",
"20260618_052611_3_20260618_052526_image.png",
"20260618_052559_2_20260618_052526_image.png",
"20260618_052548_1_20260618_052526_image.png",
"20260618_052537_0_20260618_052526_image.png",
"20260618_052526_image.png",
"20260618_051029_7_20260618_050846_img_4.png",
"20260618_051006_5_20260618_050846_img_4.png",
"20260618_050923_2_20260618_050846_img_4.png",
"20260618_050913_1_20260618_050846_img_4.png",
"20260618_050902_0_20260618_050846_img_4.png",
"20260618_015217_20260618_010710_p12-2.png",
"20260618_015207_20260618_010732_p10.png",
"20260618_015156_20260618_010700_Pasted image (4).png",
"20260618_015145_20260618_010627_jb3.png",
"20260618_015135_20260618_011424_20260618_004356_bb_01.png",
"20260618_015113_20260618_004952_jb.png",
"20260618_015030_20260618_004858_p12.png",
"20260618_015019_20260618_010638_b1.png",
"20260618_015008_20260618_011445_20260618_004450_20260617_074413_img_73.png",
"20260618_013512_Pasted image (9).png",
"20260618_013502_kk563t.png",
"20260618_011612_20260616_003803_img_14.png",
"20260618_011517_20260618_002014_20260616_020035_img_36.png",
"20260618_011456_20260618_004439_20260617_005512_img_60.png",
"20260618_011445_20260618_004450_20260617_074413_img_73.png",
"20260618_011434_20260618_004847_20260615_150812_img_19_2.png",
"20260618_011424_20260618_004356_bb_01.png",
"20260618_010732_p10.png",
"20260618_010710_p12-2.png",
"20260618_010700_Pasted image (4).png",
"20260618_010627_jb3.png",
"20260618_004952_jb.png",
"20260618_004941_out7.png",
"20260618_004930_20260617_134041_img_84.png",
"20260618_004919_kbk99v.png",
"20260618_004909_p1.png",
"20260618_004858_p12.png",
"20260618_004847_20260615_150812_img_19_2.png",
"20260618_004512_20260617_215245_img_87.png",
"20260618_004501_20260617_134041_img_84.png",
"20260618_004450_20260617_074413_img_73.png",
"20260618_004439_20260617_005512_img_60.png",
"20260618_004428_20260616_015949_img_37.png",
"20260618_004417_20260616_004250_img_18.png",
"20260618_004356_bb_01.png",
"20260618_004345_p1.png",
"20260618_004334_Pasted image (3).png",
"20260618_004324_jb.png",
"20260618_004313_20260616_021938_20160903_200935.jpg",
"20260618_004003_jb.png",
"20260618_003952_20260616_021938_20160903_200935.jpg",
"20260616_021938_20160903_200935.jpg",
"20260618_002606_20260616_021938_20160903_200935.jpg",
"20260618_002036_20260616_015949_img_37.png",
"20260618_002025_20260616_020020_img_35.png",
"20260618_002014_20260616_020035_img_36.png",
"20260617_215245_img_87.png",
"20260617_134615_img_86.png",
"20260617_134229_img_83.png",
"20260617_134041_img_84.png",
"20260617_133917_img_82.png",
"20260617_133832_img_81.png",
"20260617_133519_img_80.png",
"20260617_133411_img_79.png",
"20260617_133154_img_78.png",
"20260617_133129_img_77.png",
"20260617_074413_img_73.png",
"20260617_074223_img_72.png",
"20260617_005512_img_60.png",
"20260617_005200_img_59.png",
"20260617_004814_img_57.png",
"20260616_023306_img_52.png",
"20260616_023209_img_51.png",
"20260616_022543_img_50.png",
"20260616_022349_img_48.png",
"20260616_021003_img_41.png",
"20260616_020059_img_38.png",
"20260616_020035_img_36.png",
"20260616_020020_img_35.png",
"20260616_015949_img_37.png",
"20260616_015341_img_32.png",
"20260616_014757_img_31.png",
"20260616_004250_img_18.png",
"20260616_003916_img_15.png",
"20260616_003803_img_14.png",
"20260615_152252_imgxxx.png",
"20260615_151829_img_92.png",
"20260615_151614_img_93.png",
"20260615_150812_img_19_2.png",
"20260615_150340_test.png",
"20260615_145017_img_93.png",
"b1.png",
"jb3.png",
"jb.png",
"jb1.png",
"img.png",
"out7.png",
"out5.png",
"out4.png",
"out3.png",
"out2.png",
"out.png",
"bb_01.png",
"tp236b.png",
"pass-1.png",
"t159zr-1.png",
"kbk99v.png",
"kk563t.png",
"20260626_003137_dup_20260626_003125_dup_Pasted image (9).png",
"Pasted image (9).png",
"Pasted image (4).png",
"Pasted image (3).png",
"Pasted image.png",
"pa01.png",
"pa0.png",
"p13.png",
"p12-2.png",
"p12.png",
"p10.png",
"p1.png",
"img_2.png",
"img_3.png"
];
// --- HYDRATION_END ---
// Supported extensions
const EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.mp4', '.mov', '.webm'];
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.webm', '.avi', '.mkv']);
function isVideo(name) {
if (!name || typeof name !== 'string') return false;
return VIDEO_EXTENSIONS.has((name.split('.').pop() ? '.'+name.split('.').pop().toLowerCase() : ''));
}
// Videos can't render as an <img> thumbnail over file://, so the backend
// writes a sibling "<stem>.jpg" poster. Map a video URL → its poster URL
// (preserving the ?t= cache-buster). Non-video URLs pass through unchanged.
function posterFor(url) {
if (!url) return url;
const q = url.indexOf('?');
const path = q === -1 ? url : url.slice(0, q);
const qs = q === -1 ? '' : url.slice(q);
if (!/\.(mp4|mov|webm|avi|mkv)$/i.test(path)) return url;
// For turntable/orbit videos, use the first view frame as the poster/thumbnail instead of a non-existent .jpg
if (path.includes('_turntable/')) {
const lastSlash = path.lastIndexOf('/');
if (lastSlash !== -1) {
const dir = path.substring(0, lastSlash);
return dir + '/views/view_000_000deg.png' + qs;
}
}
return path.replace(/\.[^.]+$/, '.jpg') + qs;
}
// Auto-refresh interval in seconds (default: 120 = 2 minutes)
let REFRESH_INTERVAL = 120;
// ============================================
let autoRefreshTimer = null;
let knownFiles = new Set();
let currentFiles = [];
let currentGroups = [];
const groupData = new Map(); // gid → { urls[], names[] }
const fileGroups = {}; // filename → gid (for jumpToImage)
const cycleTimers = new Map(); // gid → intervalId
const cycleIdx = new Map(); // gid → current index
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
let _prevGid = null; // for "Back" button in studio
let imageNames = {}; // filename → human name (from /names)
let clipDescriptions = {}; // filename → description
let customGroups = {}; // filename → groupId (from /groups)
let groupNames = {}; // group_id → group display name
let fileSortOrders = {}; // filename → sort_order (integer or null)
let filePoses = {}; // filename → pose name
let filePrompts = {}; // filename → generation prompt
let fileHidden = {}; // filename → boolean (hidden from cycling)
let fileArchived = {}; // filename → boolean
let fileTags = {}; // filename → array of strings
let fileIsSource = {}; // filename → boolean
let _archiveViewActive = false; // when true, show archived items only
let fileHasBg = {}; // filename → boolean (has background, default true)
let fileSourceRefs = {}; // filename → array of source filenames
let fileHasClothing = {}; // filename → boolean|null (null = unknown)
let fileContentType = {}; // filename → 'image'|'video'
let fileFaceswapSrcVideo = {}; // filename → source video name
let privacyMode = localStorage.getItem('privacyMode') === 'true';
// ---- faceswap state ----
let _fsModelFilename = null; // model image selected for faceswap
let _fsSelectedVideo = null; // chosen template video name
let _fsJobId = null;
let _fsJobPollTimer = null;
let availableVideos = []; // populated from /videos
let _fsActiveTab = 'swap';
let _fsSelectedPoses = new Set();
let _sbPoseFilter = ''; // live pose-name filter query (Generate tab)
let _sbSelectedAngles = new Set(); // selected camera angle names
let _sbRefIndices = []; // ordered filmstrip indices selected as multi-ref (#1, #2, #3)
let _sbRefMode = 'combine'; // with 23 refs: 'combine' → 1 multi-ref output; 'each' → one edit per ref (cross product)
let _sbWireframeRef = ''; // wireframe video name for pose guide
let _sbWireframeTime = 0.5; // normalized frame time 01
let _sbPoseEdits = {}; // poseName → edited text
let _sbPadTop = 0; // canvas padding (px) applied before generation
let _sbPadRight = 0;
let _sbPadBottom = 0;
let _sbPadLeft = 0;
let _sbPadOutpaint = false;
let _sbPadUniform = false;
const CAMERA_ANGLES = [
// Absolute camera positions
{ name: 'Front', icon: '⊙', prompt: 'Head-on straight-on full-body portrait, frontal camera, realistic, transparent background', relative: false },
{ name: '¾ Left', icon: '↙', prompt: 'Three-quarter left, camera 45° to the left, full-body portrait, realistic, transparent background', relative: false },
{ name: '¾ Right', icon: '↗', prompt: 'Three-quarter right, camera 45° to the right, full-body portrait, realistic, transparent background', relative: false },
{ name: 'Side L', icon: '◁', prompt: 'Side profile, camera 90° to the left, full-body portrait, realistic, transparent background', relative: false },
{ name: 'Side R', icon: '▷', prompt: 'Side profile, camera 90° to the right, full-body portrait, realistic, transparent background', relative: false },
{ name: 'Back', icon: '⊕', prompt: 'Rear view, camera directly behind subject, full-body portrait, realistic, transparent background', relative: false },
{ name: 'High', icon: '△', prompt: "Bird's-eye view, camera above looking down, full-body, realistic, transparent background", relative: false },
{ name: 'Low', icon: '▽', prompt: "Worm's-eye view, camera below looking up, full-body portrait, realistic, transparent background", relative: false },
// Relative rotations — applied on top of current view
{ name: '↺ 45°', icon: '↺', prompt: 'Rotate 45° to the left from current view, full-body portrait, realistic, transparent background', relative: true },
{ name: '↻ 45°', icon: '↻', prompt: 'Rotate 45° to the right from current view, full-body portrait, realistic, transparent background', relative: true },
{ name: '↺ 90°', icon: '⟲', prompt: 'Rotate 90° to the left from current view, side profile, full-body portrait, realistic, transparent background', relative: true },
{ name: '↻ 90°', icon: '⟳', prompt: 'Rotate 90° to the right from current view, side profile, full-body portrait, realistic, transparent background', relative: true },
];
let _sbGenJobId = null;
let _sbGenJobPollTimer= null;
let _followLatestGid = null; // set to gid after generation to auto-jump to newest image
let _followLatestFilename = null;
let _slideshowActive = false;
let _slideshowTimer = null;
let _slideshowInterval= 4000;
let _fsTrimVideo = null;
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function getBaseName(name) {
return name.replace(/^(\d{8}_\d{6}_)+/, '');
}
function groupFiles(fileObjects) {
const map = new Map(); // gid → files[]
for (const f of fileObjects) {
const custom = customGroups[f.cleanName];
let gid;
if (custom) {
gid = custom.startsWith('solo:') ? f.cleanName : custom;
} else {
gid = getBaseName(f.cleanName);
}
if (!map.has(gid)) map.set(gid, []);
map.get(gid).push(f);
fileGroups[f.cleanName] = gid;
}
// Sort each group: by explicit sort_order first, then by filename length
for (const files of map.values()) {
files.sort((a, b) => {
const oa = fileSortOrders[a.cleanName] ?? 9999;
const ob = fileSortOrders[b.cleanName] ?? 9999;
if (oa !== ob) return oa - ob;
return a.cleanName.length - b.cleanName.length;
});
}
return [...map.entries()].map(([gid, files]) => ({ gid, files }));
}
function groupDisplayName(group) {
// 1. Explicit group_name set via UI
if (groupNames[group.gid]) return groupNames[group.gid];
// 2. Human name tagged for any file in the group
for (const f of group.files) {
if (imageNames[f.cleanName]) return imageNames[f.cleanName];
}
return group.gid;
}
function clearCycleTimers() {
cycleTimers.forEach(id => clearInterval(id));
cycleTimers.clear();
cycleIdx.clear();
}
function startGroupCycle(base) {
cycleIdx.set(base, 0); // index into visibleIdx array
const id = setInterval(() => {
// Don't update gallery cards while studio is open — saves GPU and avoids distraction
if (document.getElementById('studio').classList.contains('open')) return;
const data = groupData.get(base);
if (!data || !data.visibleIdx || data.visibleIdx.length < 2) { clearInterval(id); cycleTimers.delete(base); return; }
const card = [...document.querySelectorAll('.image-card')].find(c => c.dataset.group === base);
if (!card) { clearInterval(id); cycleTimers.delete(base); return; }
const nextSlot = (cycleIdx.get(base) + 1) % data.visibleIdx.length;
cycleIdx.set(base, nextSlot);
const realIdx = data.visibleIdx[nextSlot];
const img = card.querySelector('img');
if (img) img.src = data.urls[realIdx];
card.querySelectorAll('.dot').forEach((d, i) => d.classList.toggle('active', i === nextSlot));
}, 1800);
cycleTimers.set(base, id);
}
// --- studio view (replaces lightbox) ---
let lbUrls = [], lbNames = [], lbIdx = 0;
let _activeFilmstripTab = 'group'; // 'group', 'hidden', 'source', 'archived'
let _multiSelectModeActive = false;
const _selectedFilenames = new Set();
function switchFilmstripTab(tab) {
_activeFilmstripTab = tab;
// Highlight tab
['Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === tab);
});
// Reset selection when switching tabs
_selectedFilenames.clear();
updateMultiSelectUI();
const currentImg = lbNames[lbIdx];
if (tab === 'group') {
const data = groupData.get(lbCurrentGid);
if (data) {
lbUrls = data.urls;
lbNames = data.names;
}
} else {
const matched = [];
knownFiles.forEach(fname => {
const isArchived = !!fileArchived[fname];
const isHidden = !!fileHidden[fname];
const isSource = !!fileIsSource[fname];
if (tab === 'hidden' && isHidden && !isArchived) {
matched.push(fname);
} else if (tab === 'source' && isSource && !isArchived) {
matched.push(fname);
} else if (tab === 'archived' && isArchived) {
matched.push(fname);
}
});
// Sort matching files alphabetically (descending time-like sort)
matched.sort((a, b) => b.localeCompare(a));
lbNames = matched;
lbUrls = matched.map(n => IMAGE_FOLDER + n);
}
let newIdx = lbNames.indexOf(currentImg);
if (newIdx === -1) newIdx = 0;
lbIdx = newIdx;
updateStudio();
}
function toggleMultiSelectMode() {
_multiSelectModeActive = !_multiSelectModeActive;
const btn = document.getElementById('multiSelectToggleBtn');
if (btn) {
btn.textContent = _multiSelectModeActive ? 'Cancel Multi' : 'Multi-select';
btn.style.background = _multiSelectModeActive ? '#1e3a8a' : '#222';
}
_selectedFilenames.clear();
updateMultiSelectUI();
updateStudio(); // Re-render thumbnails to show checkmarks
}
function updateMultiSelectUI() {
const actionsEl = document.getElementById('multiSelectActions');
const countEl = document.getElementById('multiSelectCount');
if (actionsEl && countEl) {
if (_multiSelectModeActive && _selectedFilenames.size > 0) {
actionsEl.style.display = 'flex';
countEl.textContent = `${_selectedFilenames.size} selected`;
} else {
actionsEl.style.display = 'none';
}
}
}
async function bulkMoveSelected(targetStrip) {
if (_selectedFilenames.size === 0) return;
const list = Array.from(_selectedFilenames);
try {
const r = await fetch(`${API}/images/bulk-move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames: list, target_strip: targetStrip })
});
if (r.ok) {
showToast(`Moved ${list.length} images to ${targetStrip}`);
// Update local cache immediately
list.forEach(fname => {
if (targetStrip === 'VISIBLE') {
fileArchived[fname] = false;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'HIDDEN');
if (!fileTags[fname].includes('VISIBLE')) fileTags[fname].push('VISIBLE');
}
} else if (targetStrip === 'HIDDEN') {
fileArchived[fname] = false;
fileHidden[fname] = true;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'VISIBLE');
if (!fileTags[fname].includes('HIDDEN')) fileTags[fname].push('HIDDEN');
}
} else if (targetStrip === 'ARCHIVED') {
fileArchived[fname] = true;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'HIDDEN' && t !== 'VISIBLE');
if (!fileTags[fname].includes('ARCHIVED')) fileTags[fname].push('ARCHIVED');
}
} else if (targetStrip === 'SOURCE') {
fileIsSource[fname] = true;
if (fileTags[fname] && !fileTags[fname].includes('SOURCE')) {
fileTags[fname].push('SOURCE');
}
}
});
toggleMultiSelectMode();
switchFilmstripTab(_activeFilmstripTab);
refreshNow();
} else {
const err = await r.text();
showToast(`Failed to move: ${err}`, 'error');
}
} catch (e) {
showToast(`Network error: ${e}`, 'error');
}
}
function filmstripThumbDragStart(event, fname) {
event.dataTransfer.setData('text/plain', fname);
event.dataTransfer.effectAllowed = 'move';
}
async function filmstripTabDrop(event, targetStrip) {
event.preventDefault();
const fname = event.dataTransfer.getData('text/plain');
if (!fname) return;
try {
const r = await fetch(`${API}/images/bulk-move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames: [fname], target_strip: targetStrip })
});
if (r.ok) {
showToast(`Moved ${fname} to ${targetStrip}`);
if (targetStrip === 'VISIBLE') {
fileArchived[fname] = false;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'HIDDEN');
if (!fileTags[fname].includes('VISIBLE')) fileTags[fname].push('VISIBLE');
}
} else if (targetStrip === 'HIDDEN') {
fileArchived[fname] = false;
fileHidden[fname] = true;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'VISIBLE');
if (!fileTags[fname].includes('HIDDEN')) fileTags[fname].push('HIDDEN');
}
} else if (targetStrip === 'ARCHIVED') {
fileArchived[fname] = true;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'HIDDEN' && t !== 'VISIBLE');
if (!fileTags[fname].includes('ARCHIVED')) fileTags[fname].push('ARCHIVED');
}
} else if (targetStrip === 'SOURCE') {
fileIsSource[fname] = true;
if (fileTags[fname] && !fileTags[fname].includes('SOURCE')) {
fileTags[fname].push('SOURCE');
}
}
switchFilmstripTab(_activeFilmstripTab);
refreshNow();
}
} catch (e) {
showToast(`Drag-drop move failed: ${e}`, 'error');
}
}
async function toggleSourceKeybind() {
const fname = lbNames[lbIdx];
if (!fname) return;
const newSource = !fileIsSource[fname];
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: newSource })
});
if (r.ok) {
fileIsSource[fname] = newSource;
if (newSource) {
if (fileTags[fname] && !fileTags[fname].includes('SOURCE')) fileTags[fname].push('SOURCE');
} else {
if (fileTags[fname]) fileTags[fname] = fileTags[fname].filter(t => t !== 'SOURCE');
}
showToast(newSource ? 'Marked as SOURCE' : 'Unmarked as SOURCE');
updateStudio();
refreshNow();
}
} catch (err) {
showToast(`Failed to toggle source: ${err}`, 'error');
}
}
async function estimate21PlusKeybind() {
const fname = lbNames[lbIdx];
if (!fname) return;
showToast('Estimating 21+ tag...', 'info');
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/estimate-21plus`, {
method: 'POST'
});
if (r.ok) {
const data = await r.json();
const is21 = !!data['21+'];
fileTags[fname] = data.tags;
showToast(is21 ? 'Tagged as 21+' : 'Not tagged as 21+');
updateStudio();
refreshNow();
}
} catch (err) {
showToast(`Failed to estimate: ${err}`, 'error');
}
}
let _activeSidebarTab = localStorage.getItem('studioSidebarTab') || 'info';
// Maps fname → nobg sidecar URL; viewer shows sidecar while this entry exists
const _nobgSidecars = new Map();
function openLightbox(gid, startIdx) { // kept for compat
openStudio(gid, startIdx);
}
function closeLightbox() { closeStudio(); }
function openStudio(gid, startIdx) {
const data = groupData.get(gid);
if (!data) return;
if (lbCurrentGid !== gid) { // clear refs when switching groups
_prevGid = lbCurrentGid;
_sbRefIndices = [];
_sceneRefs = [null, null, null];
_scenePersonInit = false;
_sceneFrameBytes = null;
stopOrbitFast();
}
lbCurrentGid = gid;
_lastSelectedGid = gid;
localStorage.setItem('lastSelectedGid', gid);
_activeFilmstripTab = 'group';
['Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === 'group');
});
_multiSelectModeActive = false;
_selectedFilenames.clear();
updateMultiSelectUI();
lbUrls = data.urls;
lbNames = data.names;
lbIdx = startIdx !== undefined ? startIdx : (cycleIdx.get(gid) || 0);
updateStudio();
document.getElementById('studio').classList.add('open');
// Hide main page scrollbar — nothing in studio scrolls the page
document.body.style.overflow = 'hidden';
// Restore sidebar state
const sidebar = document.getElementById('studioSidebar');
const btn = document.getElementById('sidebarInnerToggle');
const wasCollapsed = localStorage.getItem('studioSidebarCollapsed') === '1';
sidebar.classList.toggle('collapsed', wasCollapsed);
if (btn) btn.textContent = wasCollapsed ? '⊞' : '⊟';
// Render current tab
switchSidebarTab(_activeSidebarTab, true);
}
/** Jump to a specific filename, switching groups if needed */
function jumpToImage(filename) {
const gid = fileGroups[filename];
if (!gid) { showToast('Image not found', 'error'); return; }
const data = groupData.get(gid);
if (!data) {
// If the group isn't in groupData (stale or archived), try to find it
// but usually groupData is kept in sync.
showToast('Group not loaded', 'error');
return;
}
const idx = data.names.indexOf(filename);
if (idx === -1) { showToast('Image not found in group', 'error'); return; }
openStudio(gid, idx);
}
function jumpToPrevGroup() {
if (_prevGid) openStudio(_prevGid);
}
function formatPadValue(v) {
if (v === undefined || v === null || v === '') return '0.00%';
const s = v.toString().trim().toLowerCase();
let val = parseFloat(s);
if (isNaN(val)) return '0.00%';
// If the user explicitly typed px
if (s.endsWith('px')) {
return val.toString(); // integer pixels
}
// If the user explicitly typed %
if (s.endsWith('%')) {
return val.toFixed(2) + '%';
}
// If contains a decimal, or if it's <= 10 (or < 1.0), it's percentage
if (s.includes('.') || val <= 10) {
// If it's a fraction like 0.1, convert to 10%
if (val > 0 && val < 1.0) {
return (val * 100).toFixed(2) + '%';
}
return val.toFixed(2) + '%';
}
// Otherwise, it's integer > 10, treat as pixels
return val.toString();
}
function syncPadFields(el) {
const id = el.id; // padLeft, padTop, etc. or sbPadLeft, sbPadTop
const val = el.value;
const prefix = id.startsWith('sbPad') ? 'sbPad' : 'pad';
const isSb = id.startsWith('sbPad');
// Check if Uniform mode is active for this context
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
const isUniform = uniformEl ? uniformEl.checked : false;
if (isUniform) {
// Set all four sides to the same value
['Top', 'Right', 'Bottom', 'Left'].forEach(side => {
const targetId = prefix + side;
const targetEl = document.getElementById(targetId);
if (targetEl && targetEl !== el) {
targetEl.value = val;
}
});
} else {
// Standard opposite-side sync if other side is 0
let pairId = null;
if (id.endsWith('Left')) pairId = id.replace('Left', 'Right');
else if (id.endsWith('Right')) pairId = id.replace('Right', 'Left');
else if (id.endsWith('Top')) pairId = id.replace('Top', 'Bottom');
else if (id.endsWith('Bottom')) pairId = id.replace('Bottom', 'Top');
if (pairId) {
const pairEl = document.getElementById(pairId);
if (pairEl && (pairEl.value === '0.00' || pairEl.value === '0' || pairEl.value === '0.00%' || pairEl.value === '')) {
pairEl.value = val;
}
}
}
if (isSb) {
updateSbGenBtn();
}
updatePadPreview();
}
function togglePadUniform(cb) {
const isSb = cb.id.startsWith('sbPad');
const prefix = isSb ? 'sbPad' : 'pad';
if (isSb) _sbPadUniform = cb.checked;
if (cb.checked) {
// Find a non-zero value, or default to Top, and propagate
const sides = ['Top', 'Right', 'Bottom', 'Left'];
let val = '0.00%';
for (let side of sides) {
const el = document.getElementById(prefix + side);
if (el && el.value && el.value !== '0' && el.value !== '0.00' && el.value !== '0.00%') {
val = el.value;
break;
}
}
sides.forEach(side => {
const el = document.getElementById(prefix + side);
if (el) el.value = val;
});
updatePadPreview();
}
}
function applyPadPreset(presetVal, isSb) {
const prefix = isSb ? 'sbPad' : 'pad';
const sides = ['Top', 'Right', 'Bottom', 'Left'];
if (presetVal === '0') {
// Reset all
sides.forEach(side => {
const el = document.getElementById(prefix + side);
if (el) el.value = '0.00%';
});
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
if (uniformEl) uniformEl.checked = false;
if (isSb) _sbPadUniform = false;
} else {
// If it ends with %
let formatted = presetVal;
if (presetVal.endsWith('%')) {
formatted = parseFloat(presetVal).toFixed(2) + '%';
} else if (presetVal.endsWith('px')) {
formatted = parseFloat(presetVal).toString();
}
sides.forEach(side => {
const el = document.getElementById(prefix + side);
if (el) el.value = formatted;
});
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
if (uniformEl) uniformEl.checked = true;
if (isSb) _sbPadUniform = true;
}
if (isSb) {
updateSbGenBtn();
}
updatePadPreview();
}
function getImageDisplayRect(img) {
if (!img || !img.naturalWidth) return null;
// If the image is zoomed, getBoundingClientRect() returns the SCALED dimensions.
// We need to account for this to find the actual visible image area.
const isZoomed = img.classList.contains('zoomed');
const scale = isZoomed ? 2.2 : 1.0;
const rect = img.getBoundingClientRect();
const container = img.parentElement;
const crect = container.getBoundingClientRect();
// The natural aspect ratio
const iw = img.naturalWidth;
const ih = img.naturalHeight;
const aspect = iw / ih;
// The container dimensions for the image (before scaling)
const cw = rect.width / scale;
const ch = rect.height / scale;
const caspect = cw / ch;
let dw, dh, dl, dt;
if (aspect > caspect) {
dw = cw;
dh = cw / aspect;
dl = 0;
dt = (ch - dh) / 2;
} else {
dh = ch;
dw = ch * aspect;
dt = 0;
dl = (cw - dw) / 2;
}
// Adjust for scale: the visual center stays the same,
// but the width/height and offsets from the top-left change.
if (isZoomed) {
const centerRelX = cw / 2;
const centerRelY = ch / 2;
// dw, dh are unscaled visual dimensions of the content.
// dl, dt are unscaled offsets within the img element's box.
// We want the scaled versions.
dw *= scale;
dh *= scale;
// The top-left of the content relative to the center of the img element:
const contentRelCenterX = dl - centerRelX;
const contentRelCenterY = dt - centerRelY;
// The top-left of the content relative to the center of the img element (scaled):
const contentRelCenterXScaled = contentRelCenterX * scale;
const contentRelCenterYScaled = contentRelCenterY * scale;
// The new offsets relative to the top-left of the scaled img element:
// Actually, rect.left/top are already for the scaled element's bounding box.
// We just need to find where the content starts within that bounding box.
const finalDl = contentRelCenterXScaled + (rect.width / 2);
const finalDt = contentRelCenterYScaled + (rect.height / 2);
return {
left: rect.left - crect.left + finalDl,
top: rect.top - crect.top + finalDt,
width: dw,
height: dh
};
}
const finalLeft = rect.left - crect.left + dl;
const finalTop = rect.top - crect.top + dt;
return { left: finalLeft, top: finalTop, width: dw, height: dh };
}
function updatePadPreview() {
const viewer = document.getElementById('studioViewer');
if (viewer) {
// Clean up existing preview overlays
viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
}
const img = document.getElementById('lbImg');
if (!viewer || !img || img.style.display === 'none') return;
const rect = getImageDisplayRect(img);
if (!rect) return;
const isManual = !!document.getElementById('padBar');
const getV = (side) => {
const el = document.getElementById(isManual ? 'pad' + side : 'sbPad' + side);
return el ? el.value : '0';
};
const fill = document.getElementById('padFill')?.value || 'transparent';
const sides = ['Left', 'Right', 'Top', 'Bottom'];
sides.forEach(side => {
const vStr = getV(side);
if (!vStr || vStr === '0' || vStr === '0.00' || vStr === '0.00%') return;
const val = parseFloat(vStr);
if (isNaN(val) || val <= 0) return;
const isPercent = vStr.includes('%');
const overlay = document.createElement('div');
overlay.className = 'pad-preview-overlay';
// Style based on selected fill mode to make preview professional & intuitive
if (isManual) {
if (fill === 'black') {
overlay.style.background = 'rgba(0, 0, 0, 0.7)';
overlay.style.borderColor = '#444';
} else if (fill === 'white') {
overlay.style.background = 'rgba(255, 255, 255, 0.7)';
overlay.style.borderColor = '#ccc';
} else {
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
overlay.style.borderColor = '#f59e0b';
}
} else {
// Sidebar is always transparent background before outpaint
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
overlay.style.borderColor = '#f59e0b';
}
if (side === 'Left' || side === 'Right') {
const w = isPercent ? (rect.width * val / 100) : val;
overlay.style.width = w + 'px';
overlay.style.top = rect.top + 'px';
overlay.style.height = rect.height + 'px';
if (side === 'Left') overlay.style.left = (rect.left - w) + 'px';
else overlay.style.left = (rect.left + rect.width) + 'px';
} else {
const h = isPercent ? (rect.height * val / 100) : val;
overlay.style.height = h + 'px';
overlay.style.left = rect.left + 'px';
overlay.style.width = rect.width + 'px';
if (side === 'Top') overlay.style.top = (rect.top - h) + 'px';
else overlay.style.top = (rect.top + rect.height) + 'px';
}
viewer.appendChild(overlay);
});
}
function closeStudio() {
document.getElementById('studio').classList.remove('open');
document.body.style.overflow = '';
stopSlideshow();
stopOrbitFast();
// Release every streaming/decoding video so closing the studio frees memory
// and stops background network streams (template clips + scenery preview).
_tplStopAll();
['sceneVideoEl', 'lbVideo', 'cltVideoPreview'].forEach(id => {
const v = document.getElementById(id);
if (v) { v.pause(); v.removeAttribute('src'); v.load(); }
});
}
function toggleSlideshow() {
if (_slideshowActive) stopSlideshow();
else startSlideshow();
}
function startSlideshow() {
if (_slideshowTimer) clearInterval(_slideshowTimer);
_slideshowActive = true;
const btn = document.getElementById('slideshowBtn');
if (btn) { btn.textContent = '⏸ Pause'; btn.style.color = '#60a5fa'; }
_slideshowTimer = setInterval(() => {
if (lbUrls.length > 1) { lbIdx = (lbIdx + 1) % lbUrls.length; updateStudio(); }
}, _slideshowInterval);
}
function stopSlideshow() {
_slideshowActive = false;
clearInterval(_slideshowTimer);
_slideshowTimer = null;
const btn = document.getElementById('slideshowBtn');
if (btn) { btn.textContent = '▶ Slideshow'; btn.style.color = ''; }
}
function updateSlideshowInterval() {
const sel = document.getElementById('slideshowInterval');
if (sel) _slideshowInterval = parseInt(sel.value, 10);
if (_slideshowActive) startSlideshow(); // restart with new interval
}
let _orbitFastActive = false;
let _orbitFastTimer = null;
function toggleOrbitFast() {
if (_orbitFastActive) stopOrbitFast();
else startOrbitFast();
}
function startOrbitFast() {
if (_slideshowActive) stopSlideshow();
_orbitFastActive = true;
const btn = document.getElementById('lbOrbitFastBtn');
if (btn) { btn.textContent = '⏸ Orbit'; btn.style.color = '#f59e0b'; }
if (_orbitFastTimer) clearInterval(_orbitFastTimer);
_orbitFastTimer = setInterval(() => {
if (lbUrls.length > 1) {
lbIdx = (lbIdx + 1) % lbUrls.length;
updateStudio();
}
}, 80); // ~12fps
}
function stopOrbitFast() {
_orbitFastActive = false;
if (_orbitFastTimer) clearInterval(_orbitFastTimer);
_orbitFastTimer = null;
const btn = document.getElementById('lbOrbitFastBtn');
if (btn) { btn.textContent = '▶ Orbit'; btn.style.color = ''; }
}
function lbNav(dir) {
lbIdx = (lbIdx + dir + lbUrls.length) % lbUrls.length;
updateStudio();
}
function lbNavTo(idx) {
lbIdx = idx;
updateStudio();
}
// Reference badge number for filmstrip thumb i (0 = none).
// Generate tab → position in _sbRefIndices; Scenery tab → slot index in _sceneRefs.
function _filmstripRefBadge(i) {
if (_activeSidebarTab === 'scenery') {
const name = lbNames[i];
const slot = _sceneRefs.findIndex(r => r && r.kind === 'file' && r.name === name);
return slot >= 0 ? slot + 1 : 0;
}
const pos = _sbRefIndices.indexOf(i);
return pos >= 0 ? pos + 1 : 0;
}
function sbFilmstripClick(event, i) {
const fname = lbNames[i];
if (_multiSelectModeActive) {
event.preventDefault();
if (_selectedFilenames.has(fname)) {
_selectedFilenames.delete(fname);
} else {
_selectedFilenames.add(fname);
}
updateMultiSelectUI();
updateStudio();
return;
}
if (event.shiftKey) {
event.preventDefault();
// Scenery tab uses positional slots (fills right→left); Generate uses _sbRefIndices.
if (_activeSidebarTab === 'scenery') {
sceneAssignRef(lbNames[i]); // re-renders panel + filmstrip
return;
}
const pos = _sbRefIndices.indexOf(i);
if (pos >= 0) {
_sbRefIndices.splice(pos, 1);
} else if (_sbRefIndices.length < 3) {
_sbRefIndices.push(i);
} else {
showToast('Max 3 references — Shift+Click a selected one to remove it', 'info');
}
updateStudio();
if (_activeSidebarTab === 'generate') renderSidebarGenerate();
} else {
lbNavTo(i);
}
}
function updateStudio() {
const fname = lbNames[lbIdx];
if (document.getElementById('poseCanvas')) _removePoseOverlay(); // skeleton is per-image
document.getElementById('poseResults')?.remove();
const lbImgEl = document.getElementById('lbImg');
const lbVideoEl = document.getElementById('lbVideo');
if (!fname) {
lbImgEl.style.display = 'none';
lbImgEl.src = '';
lbVideoEl.style.display = 'none';
lbVideoEl.src = '';
const dimEl = document.getElementById('lbImgDims');
if (dimEl) dimEl.textContent = '';
document.getElementById('lbCounter').textContent = '';
const prevBtn = document.getElementById('lbPrev');
const nextBtn = document.getElementById('lbNext');
if (prevBtn) prevBtn.style.visibility = 'hidden';
if (nextBtn) nextBtn.style.visibility = 'hidden';
const extractBtn = document.getElementById('lbExtract');
if (extractBtn) extractBtn.style.display = 'none';
const moveUp = document.getElementById('lbMoveUp');
const moveDown = document.getElementById('lbMoveDown');
if (moveUp) moveUp.style.display = 'none';
if (moveDown) moveDown.style.display = 'none';
const prefBtn = document.getElementById('lbPreferredBtn');
if (prefBtn) prefBtn.style.display = 'none';
const groupNameInput = document.getElementById('lbGroupNameInput');
if (groupNameInput) groupNameInput.value = '';
const nameInput = document.getElementById('lbNameInput');
if (nameInput) { nameInput.value = ''; nameInput.placeholder = ''; }
const clipEl = document.getElementById('lbClipDesc');
if (clipEl) clipEl.textContent = 'No images in this tab.';
const genPromptWrap = document.getElementById('lbGenPromptWrap');
if (genPromptWrap) genPromptWrap.style.display = 'none';
const poseTag = document.getElementById('lbPoseTag');
if (poseTag) poseTag.style.display = 'none';
const noBgBtn = document.getElementById('lbNoBgBtn');
const undressBtn = document.getElementById('lbUndressBtn');
const faceswapBtn = document.getElementById('lbFaceswapBtn');
if (noBgBtn) noBgBtn.style.display = 'none';
if (undressBtn) undressBtn.style.display = 'none';
if (faceswapBtn)faceswapBtn.style.display = 'none';
const cropBtn = document.getElementById('lbCropBtn');
if (cropBtn) cropBtn.style.display = 'none';
const invAlphaBtn = document.getElementById('lbInvertAlphaBtn');
if (invAlphaBtn) invAlphaBtn.style.display = 'none';
const poseBtn = document.getElementById('lbPoseBtn');
if (poseBtn) poseBtn.style.display = 'none';
const refsDiv = document.getElementById('lbSourceRefs');
if (refsDiv) refsDiv.style.display = 'none';
const strip = document.getElementById('lbVariantStrip');
if (strip) strip.innerHTML = '<div style="color:#666;font-size:11px;padding:12px;width:100%;text-align:center">No images in this strip</div>';
updateSbAngleBar();
const faceBook = document.getElementById('lbFaceBook');
if (faceBook) faceBook.style.display = 'none';
updatePadPreview();
toggleCheckerboard(false);
return;
}
lbImgEl.onload = () => {
const dimEl = document.getElementById('lbImgDims');
if (dimEl) dimEl.textContent = `${lbImgEl.naturalWidth} x ${lbImgEl.naturalHeight}`;
// Force layout recalculation and overlay sync when a new image loads
requestAnimationFrame(() => {
updatePadPreview();
const cb = document.getElementById('sbCheckerboard');
if (cb && cb.checked) toggleCheckerboard(true);
});
};
_fsModelFilename = fname; // keep faceswap/scenery/segment in sync
const isVid = isVideo(fname) || fileContentType[fname] === 'video';
// Show image dimensions
const dimEl = document.getElementById('lbImgDims');
if (dimEl) {
if (!isVid && lbImgEl.naturalWidth) {
dimEl.textContent = `${lbImgEl.naturalWidth} x ${lbImgEl.naturalHeight}`;
} else {
dimEl.textContent = '';
}
}
if (isVid) {
lbImgEl.style.display = 'none';
lbVideoEl.style.display = '';
lbVideoEl.poster = posterFor(lbUrls[lbIdx]);
lbVideoEl.src = lbUrls[lbIdx];
// Explicitly clean up pad previews and hide checkerboard when navigating to a video
updatePadPreview();
toggleCheckerboard(false);
} else {
lbVideoEl.style.display = 'none';
lbVideoEl.src = '';
lbImgEl.style.display = '';
lbImgEl.classList.remove('zoomed');
lbImgEl.src = lbUrls[lbIdx];
// If it's already in cache, onload might not fire or already fired
if (lbImgEl.complete && lbImgEl.naturalWidth) {
lbImgEl.onload();
}
}
const backBtn = document.getElementById('lbBackBtn');
if (backBtn) backBtn.style.display = _prevGid ? '' : 'none';
const groupNameInput = document.getElementById('lbGroupNameInput');
groupNameInput.value = groupNames[lbCurrentGid] || '';
const nameInput = document.getElementById('lbNameInput');
if (nameInput) { nameInput.value = imageNames[fname] || ''; nameInput.placeholder = fname; }
const clipEl = document.getElementById('lbClipDesc');
if (clipEl) clipEl.textContent = clipDescriptions[fname] || '';
const genPromptWrap = document.getElementById('lbGenPromptWrap');
const genPromptEl = document.getElementById('lbGenPrompt');
const genPrompt = filePrompts[fname] || '';
if (genPromptWrap) genPromptWrap.style.display = genPrompt ? '' : 'none';
if (genPromptEl) genPromptEl.textContent = genPrompt;
const pose = filePoses[fname];
const poseTag = document.getElementById('lbPoseTag');
if (pose) { poseTag.textContent = '⛷ ' + pose; poseTag.style.display = ''; }
else { poseTag.style.display = 'none'; }
const hidden = !!fileHidden[fname];
const studio = document.getElementById('studio');
studio.classList.toggle('st-hidden', hidden);
const eyeBtn = document.getElementById('lbEyeBtn');
if (eyeBtn) { eyeBtn.textContent = hidden ? '🚫 Hidden' : '👁 Visibility'; eyeBtn.title = hidden ? 'Show in cycling' : 'Hide from cycling'; }
const sourceBtn = document.getElementById('lbSourceBtn');
if (sourceBtn) {
const isSrc = !!fileIsSource[fname];
sourceBtn.style.color = isSrc ? '#f59e0b' : '';
sourceBtn.style.borderColor = isSrc ? '#f59e0b' : '';
sourceBtn.textContent = isSrc ? '★ Source (Yes)' : '★ Source';
}
const estimateBtn = document.getElementById('lbEstimate21Btn');
if (estimateBtn) {
const is21 = fileTags[fname] && fileTags[fname].includes('21+');
estimateBtn.style.color = is21 ? '#ef4444' : '';
estimateBtn.style.borderColor = is21 ? '#ef4444' : '';
estimateBtn.textContent = is21 ? '🔞 21+ (Yes)' : '🔞 21+';
}
const noBgBtn = document.getElementById('lbNoBgBtn');
const undressBtn = document.getElementById('lbUndressBtn');
const faceswapBtn = document.getElementById('lbFaceswapBtn');
if (noBgBtn) noBgBtn.style.display = (!isVid && fileHasBg[fname] !== false) ? '' : 'none';
if (undressBtn) undressBtn.style.display = (!isVid && fileHasClothing[fname] === true) ? '' : 'none';
if (faceswapBtn)faceswapBtn.style.display = !isVid ? '' : 'none';
const cropBtn = document.getElementById('lbCropBtn');
if (cropBtn) cropBtn.style.display = (!isVid && fileHasBg[fname] === false) ? '' : 'none';
// Invert α is only meaningful on a transparent image (after background removal).
const invAlphaBtn = document.getElementById('lbInvertAlphaBtn');
if (invAlphaBtn) invAlphaBtn.style.display = (!isVid && fileHasBg[fname] === false) ? '' : 'none';
const poseBtn = document.getElementById('lbPoseBtn');
if (poseBtn) poseBtn.style.display = isVid ? 'none' : '';
const archiveBtn = document.getElementById('lbArchiveBtn');
if (archiveBtn) archiveBtn.textContent = fileArchived[fname] ? 'Restore' : 'Archive';
// Source refs
const refs = fileSourceRefs[fname];
const refsDiv = document.getElementById('lbSourceRefs');
if (refsDiv) {
if (refs && refs.length > 0) {
refsDiv.style.display = 'flex';
refsDiv.innerHTML = refs.map(r =>
`<img src="${IMAGE_FOLDER + r}" loading="lazy" title="${r}"
onclick="jumpToImage('${r.replace(/'/g,"\\'")}');">`
).join('');
} else {
refsDiv.style.display = 'none';
}
}
// Download link
const dlBtn = document.getElementById('lbDownloadBtn');
if (dlBtn) { dlBtn.href = lbUrls[lbIdx]; dlBtn.setAttribute('download', fname); }
const multi = lbUrls.length > 1;
document.getElementById('lbCounter').textContent = multi ? `${lbIdx + 1} / ${lbUrls.length}` : '';
const prevBtn = document.getElementById('lbPrev');
const nextBtn = document.getElementById('lbNext');
if (prevBtn) prevBtn.style.visibility = multi ? '' : 'hidden';
if (nextBtn) nextBtn.style.visibility = multi ? '' : 'hidden';
const extractBtn = document.getElementById('lbExtract');
if (extractBtn) extractBtn.style.display = multi ? '' : 'none';
const moveUp = document.getElementById('lbMoveUp');
const moveDown = document.getElementById('lbMoveDown');
if (moveUp) moveUp.style.display = multi ? '' : 'none';
if (moveDown) moveDown.style.display = multi ? '' : 'none';
const prefBtn = document.getElementById('lbPreferredBtn');
if (prefBtn) prefBtn.style.display = multi && lbIdx > 0 ? '' : 'none';
// Film strip — always visible (override old display:none CSS)
const strip = document.getElementById('lbVariantStrip');
strip.style.display = 'flex';
const sceneryActive = _activeSidebarTab === 'scenery';
strip.innerHTML = lbUrls.map((url, i) => {
const n = lbNames[i];
const pose = filePoses[n] || '';
const act = i === lbIdx ? ' active' : '';
const isSel = _selectedFilenames.has(n);
const selCls = isSel ? ' selected' : '';
// Badge: Generate uses _sbRefIndices order; Scenery uses positional slots.
const badge = _filmstripRefBadge(i);
const refCls = badge > 0 ? ' ref-selected' : '';
const thumbSrc = isVideo(n) ? posterFor(url) : url;
// On the Scenery tab the thumbs are draggable onto the slots above. Otherwise draggable for strip drop targets
const dragAttr = sceneryActive
? ' draggable="true" ondragstart="sceneFilmDragStart(event,' + i + ')"'
: ' draggable="true" ondragstart="filmstripThumbDragStart(event,\'' + n.replace(/'/g, "\\'") + '\')"';
const titleStr = _multiSelectModeActive ? 'Click to select' : 'Click to view · Shift+Click to add as ref';
return '<div class="lb-var-thumb' + act + refCls + selCls + '" onclick="sbFilmstripClick(event,' + i + ')"' + dragAttr + ' title="' + titleStr + '">'
+ '<img src="' + thumbSrc + '" loading="lazy" onerror="this.style.opacity=\'0.3\'">'
+ (isVideo(n) ? '<div class="lb-var-play">▶</div>' : '')
+ (pose ? '<div class="lb-var-pose">' + escHtml(pose) + '</div>' : '')
+ (badge > 0 ? '<div class="lb-var-ref-badge">#' + badge + '</div>' : '')
+ (isSel ? '<div class="lb-var-thumb-check">✓</div>' : '')
+ '</div>';
}).join('');
const active = strip.querySelector('.lb-var-thumb.active');
if (active) active.scrollIntoView({ block: 'nearest', inline: 'center' });
// Camera angle overlay bar
updateSbAngleBar();
// Face-book thumbnail — show on slot 0 when a face crop exists for this group
const faceBook = document.getElementById('lbFaceBook');
const faceThumb = document.getElementById('lbFaceThumb');
if (faceBook && faceThumb && lbCurrentGid && lbIdx === 0 && !isVid) {
loadFaceThumb(lbCurrentGid);
} else if (faceBook) {
faceBook.style.display = 'none';
}
}
// Alias for old callsites
function updateLightbox() { updateStudio(); }
async function updateName() {
const fname = lbNames[lbIdx];
const newName = document.getElementById('lbNameInput').value;
if (newName === (imageNames[fname] || '')) return;
try {
const r = await fetch(`${API}/names/${fname}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (r.ok) {
imageNames[fname] = newName;
showToast(`Name updated`);
}
} catch (e) {
showToast(`Failed to update name: ${e}`, 'error');
}
}
async function updateGroupName() {
const gid = lbCurrentGid;
if (!gid) return;
const newName = document.getElementById('lbGroupNameInput').value.trim();
if (newName === (groupNames[gid] || '')) return;
try {
const r = await fetch(`${API}/group-names/${encodeURIComponent(gid)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (r.ok) {
if (newName) groupNames[gid] = newName;
else delete groupNames[gid];
showToast('Group name updated');
// Refresh gallery labels without full reload
document.querySelectorAll('.image-card').forEach(card => {
if (card.dataset.group === gid) {
const group = currentGroups.find(g => g.gid === gid);
if (group) card.querySelector('.image-name').textContent = groupDisplayName(group);
}
});
}
} catch (e) {
showToast(`Failed: ${e}`, 'error');
}
}
async function lbMoveInGroup(dir) {
const gid = lbCurrentGid;
const data = groupData.get(gid);
if (!data || data.names.length < 2) return;
const newIdx = lbIdx + dir;
if (newIdx < 0 || newIdx >= data.names.length) return;
// Swap in local arrays
[data.names[lbIdx], data.names[newIdx]] = [data.names[newIdx], data.names[lbIdx]];
[data.urls[lbIdx], data.urls[newIdx]] = [data.urls[newIdx], data.urls[lbIdx]];
// Update sort orders locally
data.names.forEach((fn, i) => { fileSortOrders[fn] = i; });
// Persist to API
try {
await fetch(`${API}/groups/${encodeURIComponent(gid)}/order`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames: data.names })
});
} catch (e) { /* non-fatal */ }
lbIdx = newIdx;
lbUrls = data.urls;
lbNames = data.names;
updateLightbox();
}
async function lbMoveToPosition(pos) {
const gid = lbCurrentGid;
const data = groupData.get(gid);
if (!data || data.names.length < 2) return;
// Get current index and target position
const currentIndex = lbIdx;
let targetIndex;
if (pos === 1) {
// Move to second position (index 1)
targetIndex = 1;
} else if (pos === -2) {
// Move to before last position (second to last)
targetIndex = data.names.length - 2;
} else {
return; // Invalid position
}
// Make sure target index is valid
if (targetIndex < 0 || targetIndex >= data.names.length) return;
// If already at target position, do nothing
if (currentIndex === targetIndex) return;
// Create a copy of the names array to work with
const newNames = [...data.names];
// Remove the item from its current position
const [movedItem] = newNames.splice(currentIndex, 1);
// Insert the item at the target position
newNames.splice(targetIndex, 0, movedItem);
// Update the group data with new order
data.names = newNames;
// Update sort orders locally
data.names.forEach((fn, i) => { fileSortOrders[fn] = i; });
// Persist to API
try {
await fetch(`${API}/groups/${encodeURIComponent(gid)}/order`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames: data.names })
});
} catch (e) { /* non-fatal */ }
// Update the current index to reflect new position
lbIdx = targetIndex;
lbUrls = data.urls;
lbNames = data.names;
updateLightbox();
}
async function lbToggleHidden() {
const fname = lbNames[lbIdx];
const newHidden = !fileHidden[fname];
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/hidden`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hidden: newHidden }),
});
if (!r.ok) { showToast('Failed to update visibility', 'error'); return; }
fileHidden[fname] = newHidden;
// Update visibleIdx for this group
const data = groupData.get(lbCurrentGid);
if (data) {
data.visibleIdx = data.names.map((n, i) => i).filter(i => !fileHidden[data.names[i]]);
// Restart or stop cycle accordingly
const timer = cycleTimers.get(lbCurrentGid);
if (timer) { clearInterval(timer); cycleTimers.delete(lbCurrentGid); }
if (data.visibleIdx.length > 1) startGroupCycle(lbCurrentGid);
// Update card count badge
const card = [...document.querySelectorAll('.image-card')].find(c => c.dataset.group === lbCurrentGid);
if (card) {
const vc = card.querySelector('.variant-count');
if (vc) {
const vn = data.visibleIdx.length;
const tot = data.names.length;
if (vn > 1) {
vc.innerHTML = `${vn}×${tot !== vn ? `<span style="opacity:.5;font-size:9px"> /${tot}</span>` : ''}`;
} else {
vc.remove();
}
}
}
}
showToast(newHidden ? 'Hidden from cycling' : 'Visible in cycling');
updateLightbox();
} catch (e) {
showToast('API not reachable', 'error');
}
}
// Two-step inline confirmation: first click arms the button, second confirms.
let _deleteArmed = false, _deleteArmTimer = null;
function lbDeleteArm() {
const btn = document.getElementById('lbDeleteBtn');
if (!btn) return;
if (_deleteArmed) { lbDeleteConfirm(); return; }
_deleteArmed = true;
btn.textContent = 'Confirm delete';
btn.style.background = '#dc2626';
clearTimeout(_deleteArmTimer);
_deleteArmTimer = setTimeout(() => {
_deleteArmed = false;
if (btn) { btn.textContent = 'Delete'; btn.style.background = ''; }
}, 3500);
}
async function lbDeleteConfirm() {
_deleteArmed = false;
clearTimeout(_deleteArmTimer);
const btn = document.getElementById('lbDeleteBtn');
if (btn) { btn.textContent = 'Delete'; btn.style.background = ''; btn.disabled = true; }
const fname = lbNames[lbIdx];
try {
const r = await fetch(`${API}/images/${fname}`, { method: 'DELETE' });
if (r.ok) {
showToast(`Deleted`, 'success');
if (btn) btn.disabled = false;
// Remove from current group view
lbUrls.splice(lbIdx, 1);
lbNames.splice(lbIdx, 1);
if (lbNames.length === 0) {
closeStudio();
// Add a small delay to allow static regeneration to complete before refresh
setTimeout(refreshNow, 500);
} else {
lbIdx = Math.min(lbIdx, lbNames.length - 1);
updateStudio();
// Add a small delay to allow static regeneration to complete before refresh
setTimeout(refreshNow, 500);
}
} else {
showToast(`Failed to delete`, 'error');
if (btn) btn.disabled = false;
}
} catch (e) {
showToast(`Failed to delete: ${e}`, 'error');
if (btn) btn.disabled = false;
}
}
async function lbArchive() {
const fname = lbNames[lbIdx];
if (!fname) return;
const btn = document.getElementById('lbArchiveBtn');
if (btn) btn.disabled = true;
try {
const endpoint = fileArchived[fname] ? 'unarchive' : 'archive';
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/${endpoint}`, { method: 'POST' });
if (r.ok) {
fileArchived[fname] = !fileArchived[fname];
const action = fileArchived[fname] ? 'Archived' : 'Restored';
showToast(`${action}: ${fname}`, 'success');
// Remove from current filmstrip view and navigate
lbUrls.splice(lbIdx, 1);
lbNames.splice(lbIdx, 1);
if (lbNames.length === 0) {
closeStudio();
// Add a small delay to allow static regeneration to complete before refresh
setTimeout(refreshNow, 500);
} else {
lbIdx = Math.min(lbIdx, lbNames.length - 1);
updateStudio();
// Add a small delay to allow static regeneration to complete before refresh
setTimeout(refreshNow, 500);
}
} else {
showToast('Archive failed', 'error');
}
} catch (e) { showToast('Archive error: ' + e, 'error'); }
if (btn) btn.disabled = false;
}
function toggleArchiveView() {
_archiveViewActive = !_archiveViewActive;
const btn = document.getElementById('archiveViewBtn');
if (btn) {
btn.textContent = _archiveViewActive ? '📦 Back to Gallery' : '📦 Archive';
btn.style.color = _archiveViewActive ? '#f59e0b' : '';
}
// Show/hide archive banner
let banner = document.getElementById('archiveBanner');
if (_archiveViewActive) {
if (!banner) {
banner = document.createElement('div');
banner.id = 'archiveBanner';
banner.style.cssText = 'background:#7c3aed22;border:1px solid #7c3aed;border-radius:6px;padding:8px 14px;font-size:12px;color:#a78bfa;margin:8px 0;text-align:center';
banner.textContent = '📦 Viewing archived items — these are hidden from the main gallery';
const gallery = document.getElementById('gallery');
if (gallery) gallery.parentNode.insertBefore(banner, gallery);
}
} else {
if (banner) banner.remove();
}
refreshNow();
}
async function lbAutoCrop() {
const fname = lbNames[lbIdx];
if (!fname) return;
const btn = document.getElementById('lbCropBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Cropping…'; }
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/autocrop`, { method: 'POST' });
if (r.ok) {
showToast('Cropped', 'success');
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
updateStudio();
} else {
showToast('Crop failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Crop failed: ' + e, 'error'); }
if (btn) { btn.disabled = false; btn.textContent = 'Crop'; }
}
async function lbRotate(degrees) {
const fname = lbNames[lbIdx];
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to rotate', 'info'); return; }
const btns = [document.getElementById('lbRotateLeftBtn'), document.getElementById('lbRotateRightBtn')];
btns.forEach(b => { if (b) b.disabled = true; });
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/rotate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ degrees }),
});
if (r.ok) {
showToast('Rotated', 'success');
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
updateStudio();
} else {
showToast('Rotate failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Rotate failed: ' + e, 'error'); }
btns.forEach(b => { if (b) b.disabled = false; });
}
// --- body-pose skeleton preview (interactive) ---
const POSE_MIN_SCORE = 0.3;
const POSE_KP_COLORS = ['#ef4444','#f97316','#eab308','#22c55e','#06b6d4','#3b82f6','#a855f7'];
function _removePoseOverlay() {
document.getElementById('poseCanvas')?.remove();
document.getElementById('poseToolbar')?.remove();
window._poseState = null;
const btn = document.getElementById('lbPoseBtn');
if (btn) btn.classList.remove('primary');
}
async function lbTogglePose() {
if (document.getElementById('poseCanvas')) { _removePoseOverlay(); return; }
const fname = lbNames[lbIdx];
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image', 'info'); return; }
const btn = document.getElementById('lbPoseBtn');
if (btn) { btn.disabled = true; btn.textContent = '…'; }
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/pose`, { method: 'POST' });
if (r.status === 501) {
showToast('Pose estimator not installed on the server (pip install rtmlib onnxruntime)', 'info', 6000);
} else if (r.ok) {
const d = await r.json();
if (!d.people || !d.people.length) showToast('No person detected', 'info');
else { _initPoseOverlay(d, fname); showToast(`Pose (${d.backend}) — drag joints to edit`, 'success'); }
} else {
showToast('Pose failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Pose failed: ' + e, 'error'); }
if (btn) { btn.disabled = false; btn.textContent = 'Pose'; }
}
function _initPoseOverlay(data, fname) {
_removePoseOverlay();
const viewer = document.getElementById('studioViewer');
const img = document.getElementById('lbImg');
if (!viewer || !img) return;
const canvas = document.createElement('canvas');
canvas.id = 'poseCanvas';
canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;cursor:grab;z-index:90;';
viewer.appendChild(canvas);
canvas.width = viewer.clientWidth;
canvas.height = viewer.clientHeight;
// object-fit:contain letterbox mapping image px ↔ canvas px.
const iW = data.width, iH = data.height;
const scale = Math.min(canvas.width / iW, canvas.height / iH);
const st = window._poseState = {
fname, canvas, ctx: canvas.getContext('2d'),
people: data.people.map(p => p.map(k => k.slice())), // deep copy (editable)
skeleton: data.skeleton, width: iW, height: iH,
scale, offX: (canvas.width - iW * scale) / 2, offY: (canvas.height - iH * scale) / 2,
drag: null,
};
// Floating toolbar (top-left so it doesn't fight the crop bar on the right).
const bar = document.createElement('div');
bar.id = 'poseToolbar';
bar.style.cssText = 'position:absolute;top:8px;left:8px;display:flex;gap:6px;z-index:102;';
bar.innerHTML =
'<button class="sb-btn" onclick="lbPoseReset()" title="Re-detect the original pose">Reset</button>'
+ '<button class="sb-btn primary" onclick="lbFindSimilarPose()" title="Find library images in this pose">Similar pose</button>'
+ '<div style="position:relative;display:inline-block">'
+ ' <button class="sb-btn" id="gestureMenuBtn" onclick="toggleGestureMenu(event)" title="Apply a gesture preset to the skeleton">Gesture ▾</button>'
+ ' <div id="gestureMenuPanel" style="display:none;position:absolute;top:100%;left:0;z-index:200;background:#18181b;border:1px solid #2a2a2a;border-radius:6px;padding:4px;box-shadow:0 6px 20px rgba(0,0,0,.7);min-width:150px;margin-top:4px"></div>'
+ '</div>'
+ '<button class="sb-btn" onclick="lbPoseFromVideo()" title="Pick a pose from a wireframe video frame">From video</button>'
+ '<button class="sb-btn" id="poseGenBtn" onclick="lbGenerateWithPose()" title="Generate the person in this pose (skeleton → Qwen image2 guide)">Generate ▸</button>';
viewer.appendChild(bar);
_buildGestureMenu();
canvas.addEventListener('mousedown', _posePointerDown);
canvas.addEventListener('mousemove', _posePointerMove);
window.addEventListener('mouseup', _posePointerUp);
_drawPose();
const btn = document.getElementById('lbPoseBtn');
if (btn) btn.classList.add('primary');
}
// --- Gesture presets: normalized [x,y,conf] per COCO-17 keypoint ---
// Defined in [0,1] image space for a portrait-oriented standing figure.
// Applied to the actual image by scaling to image pixel dimensions.
const GESTURE_PRESETS = [
{ name: 'T-pose', desc: 'Arms fully extended sideways',
kpts: [[.50,.07,1],[.47,.065,1],[.53,.065,1],[.43,.08,1],[.57,.08,1],
[.25,.22,1],[.75,.22,1],[.10,.22,1],[.90,.22,1],[-.02,.22,1],[1.02,.22,1],
[.40,.52,1],[.60,.52,1],[.40,.72,1],[.60,.72,1],[.40,.93,1],[.60,.93,1]] },
{ name: 'Wave right', desc: 'Right arm raised and waving',
kpts: [[.50,.07,1],[.47,.065,1],[.53,.065,1],[.43,.08,1],[.57,.08,1],
[.35,.22,1],[.65,.22,1],[.20,.38,1],[.78,.10,1],[.22,.52,1],[.70,.04,1],
[.40,.52,1],[.60,.52,1],[.40,.72,1],[.60,.72,1],[.40,.93,1],[.60,.93,1]] },
{ name: 'Arms raised', desc: 'Both arms raised above head',
kpts: [[.50,.07,1],[.47,.065,1],[.53,.065,1],[.43,.08,1],[.57,.08,1],
[.35,.22,1],[.65,.22,1],[.22,.08,1],[.78,.08,1],[.18,.02,1],[.82,.02,1],
[.40,.52,1],[.60,.52,1],[.40,.72,1],[.60,.72,1],[.40,.93,1],[.60,.93,1]] },
{ name: 'Hands on hips', desc: 'Hands resting on hips',
kpts: [[.50,.07,1],[.47,.065,1],[.53,.065,1],[.43,.08,1],[.57,.08,1],
[.35,.22,1],[.65,.22,1],[.28,.40,1],[.72,.40,1],[.35,.52,1],[.65,.52,1],
[.38,.52,1],[.62,.52,1],[.38,.72,1],[.62,.72,1],[.38,.93,1],[.62,.93,1]] },
{ name: 'Turn left 45°', desc: 'Body turned ~45° to the left',
kpts: [[.45,.07,1],[.44,.065,1],[.50,.065,1],[.41,.08,1],[.52,.08,1],
[.30,.22,1],[.58,.22,1],[.18,.38,1],[.65,.30,1],[.12,.52,1],[.70,.22,1],
[.36,.52,1],[.58,.52,1],[.36,.72,1],[.58,.72,1],[.36,.93,1],[.58,.93,1]] },
{ name: 'Turn right 45°', desc: 'Body turned ~45° to the right',
kpts: [[.55,.07,1],[.50,.065,1],[.56,.065,1],[.48,.08,1],[.59,.08,1],
[.42,.22,1],[.70,.22,1],[.35,.30,1],[.82,.38,1],[.30,.22,1],[.88,.52,1],
[.42,.52,1],[.64,.52,1],[.42,.72,1],[.64,.72,1],[.42,.93,1],[.64,.93,1]] },
];
function _buildGestureMenu() {
const panel = document.getElementById('gestureMenuPanel');
if (!panel) return;
panel.innerHTML = GESTURE_PRESETS.map((g, i) =>
`<div style="padding:5px 10px;cursor:pointer;font-size:11px;border-radius:4px;white-space:nowrap"
title="${escHtml(g.desc)}"
onmouseenter="this.style.background='#2a2a2a'" onmouseleave="this.style.background=''"
onclick="applyGesturePreset(${i})">${escHtml(g.name)}</div>`
).join('');
}
function toggleGestureMenu(e) {
e.stopPropagation();
const p = document.getElementById('gestureMenuPanel');
if (!p) return;
const open = p.style.display !== 'none';
p.style.display = open ? 'none' : 'block';
if (!open) {
const close = (ev) => { if (!p.contains(ev.target)) { p.style.display='none'; document.removeEventListener('click',close); } };
setTimeout(() => document.addEventListener('click', close), 0);
}
}
function applyGesturePreset(idx) {
const st = window._poseState;
if (!st || !st.people.length) { showToast('No pose overlay active — click Pose first', 'info'); return; }
const g = GESTURE_PRESETS[idx];
const W = st.width, H = st.height;
st.people[0] = g.kpts.map(([nx, ny, c]) => [nx * W, ny * H, c]);
st._gestureName = g.name; // remember for generation
_drawPose();
document.getElementById('gestureMenuPanel').style.display = 'none';
showToast(`Gesture: ${g.name} — click Generate ▸ to create`, 'success');
}
function _drawPose() {
const st = window._poseState; if (!st) return;
const { ctx, canvas, scale, offX, offY } = st;
const cx = x => offX + x * scale, cy = y => offY + y * scale;
ctx.clearRect(0, 0, canvas.width, canvas.height);
st.people.forEach((kpts, pi) => {
const color = POSE_KP_COLORS[pi % POSE_KP_COLORS.length];
ctx.lineWidth = 3; ctx.strokeStyle = color;
st.skeleton.forEach(([a, b]) => {
const pa = kpts[a], pb = kpts[b];
if (!pa || !pb || pa[2] < POSE_MIN_SCORE || pb[2] < POSE_MIN_SCORE) return;
ctx.beginPath(); ctx.moveTo(cx(pa[0]), cy(pa[1])); ctx.lineTo(cx(pb[0]), cy(pb[1])); ctx.stroke();
});
kpts.forEach((p, ki) => {
if (!p || p[2] < POSE_MIN_SCORE) return;
const hot = st.drag && st.drag.pi === pi && st.drag.ki === ki;
ctx.beginPath(); ctx.arc(cx(p[0]), cy(p[1]), hot ? 7 : 5, 0, Math.PI * 2);
ctx.fillStyle = '#fff'; ctx.fill();
ctx.lineWidth = 2; ctx.strokeStyle = color; ctx.stroke();
});
});
}
function _poseHitTest(mx, my) {
const st = window._poseState; if (!st) return null;
const cx = x => st.offX + x * st.scale, cy = y => st.offY + y * st.scale;
let best = null, bestD = 14 * 14; // ~14px grab radius
st.people.forEach((kpts, pi) => kpts.forEach((p, ki) => {
if (!p || p[2] < POSE_MIN_SCORE) return;
const dx = mx - cx(p[0]), dy = my - cy(p[1]), d = dx * dx + dy * dy;
if (d < bestD) { bestD = d; best = { pi, ki }; }
}));
return best;
}
function _poseEvtXY(e) {
const r = window._poseState.canvas.getBoundingClientRect();
return [e.clientX - r.left, e.clientY - r.top];
}
function _posePointerDown(e) {
const st = window._poseState; if (!st) return;
const [mx, my] = _poseEvtXY(e);
const hit = _poseHitTest(mx, my);
if (hit) { st.drag = hit; st.canvas.style.cursor = 'grabbing'; _drawPose(); e.preventDefault(); }
}
function _posePointerMove(e) {
const st = window._poseState; if (!st) return;
if (!st.drag) {
const [mx, my] = _poseEvtXY(e);
st.canvas.style.cursor = _poseHitTest(mx, my) ? 'grab' : 'default';
return;
}
const [mx, my] = _poseEvtXY(e);
const p = st.people[st.drag.pi][st.drag.ki];
p[0] = Math.max(0, Math.min(st.width, (mx - st.offX) / st.scale));
p[1] = Math.max(0, Math.min(st.height, (my - st.offY) / st.scale));
if (p[2] < POSE_MIN_SCORE) p[2] = 1.0; // dragging a hidden joint makes it visible
_drawPose();
}
function _posePointerUp() {
const st = window._poseState; if (!st || !st.drag) return;
st.drag = null; st.canvas.style.cursor = 'grab'; _drawPose();
}
async function lbPoseReset() {
const st = window._poseState; if (!st) return;
const fname = st.fname;
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/pose`, { method: 'POST' });
if (r.ok) { const d = await r.json(); if (d.people?.length) _initPoseOverlay(d, fname); }
} catch (e) { showToast('Reset failed: ' + e, 'error'); }
}
async function lbFindSimilarPose() {
const st = window._poseState; if (!st) return;
// Use the (possibly edited) primary skeleton.
const kpts = st.people[0];
const btn = event?.target;
if (btn) btn.disabled = true;
try {
const r = await fetch(`${API}/pose/similar`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keypoints: kpts, width: st.width, height: st.height, limit: 12 }),
});
if (r.ok) {
const d = await r.json();
_renderPoseResults(d.similar || []);
} else if (r.status === 400) {
showToast('Pose too sparse — drag more joints into place', 'info');
} else {
showToast('Similar failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Similar failed: ' + e, 'error'); }
if (btn) btn.disabled = false;
}
async function lbGenerateWithPose() {
const st = window._poseState;
if (!st || !st.people.length) { showToast('No pose overlay active — click Pose first', 'info'); return; }
const fname = st.fname;
if (!fname) { showToast('No image active', 'info'); return; }
const btn = document.getElementById('poseGenBtn');
if (btn) { btn.disabled = true; btn.textContent = '…'; }
try {
const payload = {
model_filename: fname,
keypoints: st.people[0],
width: st.width,
height: st.height,
gesture_name: st._gestureName || null,
seed: -1,
};
const r = await fetch(`${API}/generate-with-pose`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) { showToast('Generate failed: ' + await r.text(), 'error'); return; }
const { job_id } = await r.json();
showToast('Pose generation started…', 'info');
if (btn) btn.textContent = 'Generating…';
const poll = () => setTimeout(async () => {
try {
const jr = await fetch(`${API}/batch/${job_id}`);
if (!jr.ok) { poll(); return; }
const job = await jr.json();
if (job.status === 'running') { poll(); return; }
if (btn) { btn.disabled = false; btn.textContent = 'Generate ▸'; }
if (job.status === 'done') {
showToast('Pose generation done!', 'success');
_followLatestGid = lbCurrentGid;
refreshNow(true);
} else {
showToast('Pose generation failed: ' + (job.error || 'unknown'), 'error');
}
} catch (e) { poll(); }
}, 2500);
poll();
} catch (e) {
showToast('Generate error: ' + e, 'error');
if (btn) { btn.disabled = false; btn.textContent = 'Generate ▸'; }
}
}
// ─── Pose from wireframe video ───────────────────────────────────────────
// Opens a modal that lists wireframe videos for the current group,
// lets the user scrub to a timestamp, then extracts keypoints from that
// frame and loads them into the draggable pose overlay.
async function lbPoseFromVideo() {
const st = window._poseState;
if (!st) { showToast('Open Pose first', 'info'); return; }
// Gather wireframe videos from the current group's images.
const videos = (lbNames || []).filter(n => /\.(mp4|webm|mov)$/i.test(n));
if (!videos.length) {
showToast('No videos in this group — generate a wireframe video first', 'info');
return;
}
// Build modal
const overlay = document.createElement('div');
overlay.id = 'poseVideoModal';
overlay.style.cssText = 'position:fixed;inset:0;z-index:900;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = `
<div style="background:#18181b;border:1px solid #2a2a2a;border-radius:10px;padding:20px;width:480px;max-width:95vw;box-shadow:0 8px 40px rgba(0,0,0,.8)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<span style="font-size:14px;font-weight:600">Pick pose from wireframe video</span>
<button class="sb-btn" onclick="document.getElementById('poseVideoModal').remove()">✕</button>
</div>
<div style="margin-bottom:10px">
<label style="font-size:11px;color:#aaa;display:block;margin-bottom:4px">Video</label>
<select id="pvmVideoSelect" style="width:100%;background:#0f0f0f;color:#e0e0e0;border:1px solid #2a2a2a;border-radius:5px;padding:5px 8px;font-size:12px">
${videos.map(v => `<option value="${escHtml(v)}">${escHtml(v)}</option>`).join('')}
</select>
</div>
<div style="margin-bottom:10px">
<video id="pvmVideo" style="width:100%;border-radius:6px;background:#000;max-height:260px" preload="metadata"></video>
</div>
<div style="margin-bottom:14px">
<label style="font-size:11px;color:#aaa;display:block;margin-bottom:4px">Scrub to pose frame</label>
<input type="range" id="pvmScrubber" min="0" max="100" step="0.1" value="0"
style="width:100%;accent-color:#a78bfa">
<span id="pvmTimeLabel" style="font-size:11px;color:#888">0.0 s</span>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="sb-btn" onclick="document.getElementById('poseVideoModal').remove()">Cancel</button>
<button class="sb-btn primary" id="pvmLoadBtn" onclick="pvmLoadPose()">Load pose</button>
</div>
</div>`;
document.body.appendChild(overlay);
const vidEl = document.getElementById('pvmVideo');
const select = document.getElementById('pvmVideoSelect');
const scrubber = document.getElementById('pvmScrubber');
const timeLabel = document.getElementById('pvmTimeLabel');
function pvmSetVideo(filename) {
vidEl.src = IMAGE_FOLDER + filename;
vidEl.load();
vidEl.addEventListener('loadedmetadata', () => {
const dur = vidEl.duration || 10;
scrubber.max = dur;
scrubber.step = Math.max(0.033, dur / 500);
scrubber.value = 0;
timeLabel.textContent = '0.0 s';
}, { once: true });
}
scrubber.addEventListener('input', () => {
const t = parseFloat(scrubber.value);
vidEl.currentTime = t;
timeLabel.textContent = t.toFixed(2) + ' s';
});
select.addEventListener('change', () => pvmSetVideo(select.value));
pvmSetVideo(videos[0]);
window.pvmLoadPose = async function() {
const video = select.value;
const time_s = parseFloat(scrubber.value);
const btn = document.getElementById('pvmLoadBtn');
btn.disabled = true;
btn.textContent = 'Extracting…';
try {
const r = await fetch(`${API}/pose/from-wireframe`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video, time_s }),
});
if (!r.ok) { showToast('Pose extract failed: ' + await r.text(), 'error'); btn.disabled = false; btn.textContent = 'Load pose'; return; }
const d = await r.json();
// Rescale keypoints from video dimensions to current image dimensions
const scaleX = st.width / d.width;
const scaleY = st.height / d.height;
st.people[0] = d.keypoints.map(([x, y, c]) => [x * scaleX, y * scaleY, c]);
st._gestureName = null; // came from real video, not a named preset
_drawPose();
document.getElementById('poseVideoModal').remove();
showToast(`Pose loaded from ${video} @ ${time_s.toFixed(2)}s`, 'success');
} catch (e) {
showToast('Pose extract error: ' + e, 'error');
btn.disabled = false; btn.textContent = 'Load pose';
}
};
}
async function lbFindSimilarFaces() {
if (!lbCurrentGid) return;
try {
const r = await fetch(`${API}/faces/similar`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_id: lbCurrentGid, limit: 12 }),
});
if (r.status === 404) { _renderFaceResults(null); return; }
if (!r.ok) { showToast('Similar faces failed: ' + await r.text(), 'error'); return; }
const d = await r.json();
_renderFaceResults(d.similar || []);
} catch (e) { showToast('Similar faces failed: ' + e, 'error'); }
}
async function lbBuildFaceIndex() {
try {
const r = await fetch(`${API}/faces/index`, { method: 'POST' });
if (!r.ok) { showToast('Face index failed: ' + await r.text(), 'error'); return; }
showToast('Building face index…', 'info');
_pollFaceIndex();
} catch (e) { showToast('Face index failed: ' + e, 'error'); }
}
async function _pollFaceIndex() {
try {
const r = await fetch(`${API}/faces/index/status`);
if (r.ok) {
const s = await r.json();
if (s.running) {
showToast(`Face index: ${s.done}/${s.total}`, 'info', 2500);
setTimeout(_pollFaceIndex, 2000);
} else {
showToast(`Face index ready (${s.indexed} embeddings)`, 'success');
}
}
} catch (e) {}
}
function _renderFaceResults(items) {
document.getElementById('faceResults')?.remove();
const viewer = document.getElementById('studioViewer');
if (!viewer) return;
const panel = document.createElement('div');
panel.id = 'faceResults';
panel.style.cssText = 'position:absolute;left:0;right:0;bottom:0;z-index:103;background:rgba(0,0,0,0.85);'
+ 'padding:8px;display:flex;gap:8px;overflow-x:auto;align-items:center;';
if (!items || !items.length) {
const msg = items === null
? 'No face embedding for this group — build the index first'
: 'No similar faces found';
panel.innerHTML = `<span style="color:#aaa;font-size:12px;flex:1">${msg}</span>`
+ '<button class="sb-btn" onclick="lbBuildFaceIndex();document.getElementById(\'faceResults\')?.remove()">Build index</button>'
+ '<button class="sb-btn" onclick="document.getElementById(\'faceResults\')?.remove()">Close</button>';
} else {
panel.innerHTML = '<span style="color:#aaa;font-size:11px;flex-shrink:0">Similar faces:</span>'
+ items.map(it => {
const u = IMAGE_FOLDER + it.filename + '?t=' + Date.now();
const gid = (it.group_id || '').replace(/'/g, "\\'");
const fn = it.filename.replace(/'/g, "\\'");
return `<div title="dist ${it.distance}" style="flex-shrink:0;cursor:pointer;text-align:center"
onclick="openFaceResult('${gid}','${fn}')">
<img src="${u}" loading="lazy" style="width:64px;height:64px;object-fit:cover;border-radius:5px;border:1px solid #444" onerror="this.style.opacity='0.3'">
<div style="font-size:9px;color:#777">${it.distance}</div></div>`;
}).join('')
+ '<button class="sb-btn" style="flex-shrink:0" onclick="document.getElementById(\'faceResults\')?.remove()">Close</button>';
}
viewer.appendChild(panel);
}
function openFaceResult(gid, fname) {
document.getElementById('faceResults')?.remove();
if (gid && groupData.has(gid)) {
openStudio(gid, 0);
} else {
const i = lbNames.indexOf(fname);
if (i >= 0) { lbIdx = i; updateStudio(); }
else showToast('Group not found in gallery: ' + fname, 'info', 5000);
}
}
function _renderPoseResults(items) {
document.getElementById('poseResults')?.remove();
const viewer = document.getElementById('studioViewer');
if (!viewer) return;
const panel = document.createElement('div');
panel.id = 'poseResults';
panel.style.cssText = 'position:absolute;left:0;right:0;bottom:0;z-index:103;background:rgba(0,0,0,0.85);'
+ 'padding:8px;display:flex;gap:8px;overflow-x:auto;align-items:center;';
if (!items.length) {
panel.innerHTML = '<span style="color:#aaa;font-size:12px;flex:1">No similar poses found — try building the pose index.</span>'
+ '<button class="sb-btn" onclick="lbBuildPoseIndex()">Build index</button>'
+ '<button class="sb-btn" onclick="document.getElementById(\'poseResults\')?.remove()">Close</button>';
} else {
panel.innerHTML = '<span style="color:#aaa;font-size:11px;flex-shrink:0">Similar poses:</span>'
+ items.map(it => {
const u = IMAGE_FOLDER + it.filename + '?t=' + Date.now();
return `<div title="dist ${it.distance}" style="flex-shrink:0;cursor:pointer;text-align:center"
onclick="openPoseResult('${(it.group_id||'').replace(/'/g,"\\'")}','${it.filename.replace(/'/g,"\\'")}')">
<img src="${u}" loading="lazy" style="width:64px;height:64px;object-fit:cover;border-radius:5px;border:1px solid #444" onerror="this.style.opacity='0.3'">
<div style="font-size:9px;color:#777">${it.distance}</div></div>`;
}).join('')
+ '<button class="sb-btn" style="flex-shrink:0" onclick="document.getElementById(\'poseResults\')?.remove()">Close</button>';
}
viewer.appendChild(panel);
}
function openPoseResult(gid, fname) {
_removePoseOverlay();
document.getElementById('poseResults')?.remove();
if (gid && groupData.has(gid)) {
const i = groupData.get(gid).names.indexOf(fname);
openStudio(gid, i >= 0 ? i : 0); // openStudio sets lbUrls/lbNames/lbIdx + updateStudio
} else {
const i = lbNames.indexOf(fname);
if (i >= 0) { lbIdx = i; updateStudio(); }
else showToast('Open its group from the gallery: ' + fname, 'info', 5000);
}
}
async function lbBuildPoseIndex() {
try {
const r = await fetch(`${API}/pose/index`, { method: 'POST' });
if (r.status === 501) { showToast('Pose estimator not installed', 'info'); return; }
if (!r.ok) { showToast('Index failed: ' + await r.text(), 'error'); return; }
showToast('Building pose index…', 'info');
_pollPoseIndex();
} catch (e) { showToast('Index failed: ' + e, 'error'); }
}
async function _pollPoseIndex() {
try {
const r = await fetch(`${API}/pose/index/status`);
if (r.ok) {
const s = await r.json();
if (s.running) {
showToast(`Pose index: ${s.done}/${s.total}`, 'info', 2500);
setTimeout(_pollPoseIndex, 2000);
} else {
showToast(`Pose index ready (${s.indexed} images)`, 'success');
}
}
} catch (e) { /* stop polling */ }
}
function startManualCrop() {
const fname = lbNames[lbIdx];
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to crop', 'info'); return; }
const viewer = document.getElementById('studioViewer');
const img = document.getElementById('lbImg');
if (!img || img.style.display === 'none') { showToast('No image displayed', 'info'); return; }
if (document.getElementById('cropCanvas')) return; // already active
const canvas = document.createElement('canvas');
canvas.id = 'cropCanvas';
canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair;z-index:100;';
viewer.appendChild(canvas);
const bar = document.createElement('div');
bar.id = 'cropBar';
// pointer-events:none on the container so the canvas underneath captures drags
// from the very top of the image. Individual interactive controls opt back in.
bar.style.cssText = 'position:absolute;top:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.75);z-index:101;justify-content:flex-end;align-items:center;pointer-events:none;';
bar.innerHTML = '<span style="flex:1;font-size:11px;color:#aaa">Drag to select crop area</span>'
+ '<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:#ccc;cursor:pointer;pointer-events:auto" title="Keep the original and crop a copy instead">'
+ '<input type="checkbox" id="cropAsCopy" checked style="cursor:pointer">Save as copy</label>'
+ '<button class="sb-btn" style="pointer-events:auto" onclick="cancelManualCrop()">Cancel</button>'
+ '<button class="sb-btn primary" style="pointer-events:auto" id="cropConfirmBtn" disabled onclick="confirmManualCrop()">Crop</button>';
viewer.appendChild(bar);
const st = window._cropState = { canvas, viewer, img, fname, x1: 0, y1: 0, x2: 0, y2: 0, dragging: false };
function resize() { canvas.width = viewer.clientWidth; canvas.height = viewer.clientHeight; draw(); }
function draw() {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const w = Math.abs(st.x2 - st.x1), h = Math.abs(st.y2 - st.y1);
if (w < 2 || h < 2) return;
const rx = Math.min(st.x1, st.x2), ry = Math.min(st.y1, st.y2);
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.clearRect(rx, ry, w, h);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.strokeRect(rx + 0.5, ry + 0.5, w - 1, h - 1);
}
st.draw = draw;
canvas.addEventListener('mousedown', e => {
const r = canvas.getBoundingClientRect();
st.x1 = st.x2 = e.clientX - r.left;
st.y1 = st.y2 = e.clientY - r.top;
st.dragging = true;
const cb = document.getElementById('cropConfirmBtn');
if (cb) cb.disabled = true;
});
canvas.addEventListener('mousemove', e => {
if (!st.dragging) return;
const r = canvas.getBoundingClientRect();
st.x2 = e.clientX - r.left;
st.y2 = e.clientY - r.top;
draw();
});
canvas.addEventListener('mouseup', () => {
st.dragging = false;
draw();
const w = Math.abs(st.x2 - st.x1), h = Math.abs(st.y2 - st.y1);
const cb = document.getElementById('cropConfirmBtn');
if (cb) cb.disabled = (w < 5 || h < 5);
});
resize();
}
function cancelManualCrop() {
document.getElementById('cropCanvas')?.remove();
document.getElementById('cropBar')?.remove();
window._cropState = null;
}
async function confirmManualCrop() {
const st = window._cropState;
if (!st) return;
const { canvas, img, fname } = st;
// Convert canvas rect → image pixel coords (account for object-fit: contain letterbox)
const cW = canvas.width, cH = canvas.height;
const iW = img.naturalWidth, iH = img.naturalHeight;
const scale = Math.min(cW / iW, cH / iH);
const dW = iW * scale, dH = iH * scale;
const offX = (cW - dW) / 2, offY = (cH - dH) / 2;
const toImg = (cx, cy) => ({
x: Math.round(Math.max(0, Math.min(iW, (cx - offX) / scale))),
y: Math.round(Math.max(0, Math.min(iH, (cy - offY) / scale))),
});
const p1 = toImg(Math.min(st.x1, st.x2), Math.min(st.y1, st.y2));
const p2 = toImg(Math.max(st.x1, st.x2), Math.max(st.y1, st.y2));
if (p2.x - p1.x < 2 || p2.y - p1.y < 2) { showToast('Crop area too small', 'info'); return; }
const asCopy = document.getElementById('cropAsCopy')?.checked ?? false;
const btn = document.getElementById('cropConfirmBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Cropping…'; }
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/crop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, as_copy: asCopy }),
});
if (r.ok) {
const d = await r.json().catch(() => ({}));
cancelManualCrop();
if (asCopy && d.new_filename) {
showToast('Cropped to copy', 'success');
// Insert the cropped copy right after the original in the studio strip.
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + d.new_filename + '?t=' + Date.now());
lbNames.splice(lbIdx + 1, 0, d.new_filename);
lbIdx = lbIdx + 1;
updateStudio();
refreshNow();
} else {
showToast('Cropped', 'success');
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
updateStudio();
}
} else {
showToast('Crop failed: ' + await r.text(), 'error');
if (btn) { btn.disabled = false; btn.textContent = 'Crop'; }
}
} catch (e) {
showToast('Crop error: ' + e, 'error');
if (btn) { btn.disabled = false; btn.textContent = 'Crop'; }
}
}
function startManualPad() {
const fname = lbNames[lbIdx];
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to pad', 'info'); return; }
const viewer = document.getElementById('studioViewer');
if (!viewer) return;
if (document.getElementById('padBar')) return; // already active
const bar = document.createElement('div');
bar.id = 'padBar';
bar.style.cssText = 'position:absolute;top:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.85);z-index:101;justify-content:flex-end;align-items:center;flex-wrap:wrap;';
const numInput = (id, label) => {
const onInput = `syncPadFields(this)`;
const onBlur = `this.value = formatPadValue(this.value); updatePadPreview()`;
const onKeyDown = `if(event.key === 'Enter') { confirmManualPad(); event.preventDefault(); } if(event.key === 'Escape') { cancelManualPad(); event.preventDefault(); }`;
return `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0.00%" oninput="${onInput}" onblur="${onBlur}" onkeydown="${onKeyDown}" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`;
};
bar.innerHTML =
`<span style="font-size:11px;color:#aaa">Preset:</span>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('10%', false)">+10%</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('20%', false)">+20%</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('50%', false)">+50%</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:4px;" onclick="applyPadPreset('100px', false)">+100px</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:8px;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', false)">Reset</button>`
+ `<span style="font-size:11px;color:#aaa;margin-right:4px;">Custom:</span>`
+ numInput('padTop','↑')
+ numInput('padRight','→')
+ numInput('padBottom','↓')
+ numInput('padLeft','←')
+ `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc;cursor:pointer" title="Keep all padding values equal">`
+ `<input type="checkbox" id="padUniform" style="cursor:pointer" onchange="togglePadUniform(this)">Uniform</label>`
+ `<select id="padFill" onchange="updatePadPreview()" style="background:#111;border:1px solid #333;color:#aaa;border-radius:4px;font-size:11px;padding:2px 5px">
<option value="transparent">Transparent</option>
<option value="black">Black</option>
<option value="white">White</option>
</select>`
+ `<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:#ccc;cursor:pointer" title="Keep original and pad a copy">`
+ `<input type="checkbox" id="padAsCopy" checked style="cursor:pointer">Copy</label>`
+ `<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:#ccc;cursor:pointer" title="Instruct the model to fill in the padded area">`
+ `<input type="checkbox" id="padOutpaint" style="cursor:pointer">Outpaint</label>`
+ `<button class="sb-btn" onclick="cancelManualPad()">Cancel</button>`
+ `<button class="sb-btn primary" id="padConfirmBtn" onclick="confirmManualPad()">Expand</button>`;
viewer.appendChild(bar);
updatePadPreview();
}
function cancelManualPad() {
document.getElementById('padBar')?.remove();
// Clean up preview overlays
document.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
}
async function confirmManualPad() {
const fname = lbNames[lbIdx];
if (!fname) return;
const top = document.getElementById('padTop')?.value || '0';
const right = document.getElementById('padRight')?.value || '0';
const bottom = document.getElementById('padBottom')?.value || '0';
const left = document.getElementById('padLeft')?.value || '0';
const isZero = (v) => !v || v === '0' || v === '0%' || v === '0px';
if (isZero(top) && isZero(right) && isZero(bottom) && isZero(left)) { showToast('Enter at least one padding value', 'info'); return; }
const fill = document.getElementById('padFill')?.value || 'transparent';
const asCopy = document.getElementById('padAsCopy')?.checked ?? true;
const outpaint = document.getElementById('padOutpaint')?.checked ?? false;
const btn = document.getElementById('padConfirmBtn');
if (btn) { btn.disabled = true; btn.textContent = outpaint ? 'Outpainting…' : 'Padding…'; }
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/pad`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ top, right, bottom, left, fill, as_copy: asCopy, outpaint }),
});
if (r.ok) {
const d = await r.json().catch(() => ({}));
cancelManualPad();
if (asCopy && d.new_filename) {
showToast(outpaint ? 'Outpainted (copy)' : 'Padded (copy)', 'success');
const newFn = d.new_filename;
// Clone metadata locally to avoid refresh blink
filePrompts[newFn] = filePrompts[fname];
filePoses[newFn] = filePoses[fname];
fileHasBg[newFn] = outpaint ? true : fileHasBg[fname];
fileHasClothing[newFn] = fileHasClothing[fname];
fileContentType[newFn] = 'image';
fileSourceRefs[newFn] = [fname];
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
lbNames.splice(lbIdx + 1, 0, newFn);
lbIdx = lbIdx + 1;
const group = groupData.get(lbCurrentGid);
if (group) {
group.urls.splice(lbIdx, 0, lbUrls[lbIdx]);
group.names.splice(lbIdx, 0, lbNames[lbIdx]);
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
}
updateStudio();
_followLatestFilename = newFn;
_followLatestGid = lbCurrentGid;
refreshNow(true);
} else {
showToast('Padded', 'success');
_bustStudioImage(fname);
}
} else {
showToast('Pad failed: ' + await r.text(), 'error');
if (btn) { btn.disabled = false; btn.textContent = 'Expand'; }
}
} catch (e) {
showToast('Pad error: ' + e, 'error');
if (btn) { btn.disabled = false; btn.textContent = 'Pad'; }
}
}
async function lbDuplicate() {
const fname = lbNames[lbIdx];
if (!fname) return;
const btn = document.getElementById('lbDuplicateBtn');
if (btn) btn.disabled = true;
showToast('Duplicating…');
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/duplicate`, { method: 'POST' });
if (r.ok) {
const d = await r.json();
showToast('Duplicated', 'success');
const newFn = d.new_filename;
// Clone metadata locally to avoid refresh blink
filePrompts[newFn] = filePrompts[fname];
filePoses[newFn] = filePoses[fname];
fileHasBg[newFn] = fileHasBg[fname];
fileHasClothing[newFn] = fileHasClothing[fname];
fileContentType[newFn] = fileContentType[fname] || 'image';
fileSourceRefs[newFn] = [fname];
// Insert the duplicate right after the current image in the studio strip
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
lbNames.splice(lbIdx + 1, 0, newFn);
lbIdx = lbIdx + 1;
const group = groupData.get(lbCurrentGid);
if (group) {
group.urls.splice(lbIdx, 0, lbUrls[lbIdx]);
group.names.splice(lbIdx, 0, lbNames[lbIdx]);
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
}
updateStudio();
_followLatestGid = lbCurrentGid;
refreshNow(true);
} else {
showToast('Duplicate failed', 'error');
}
} catch (e) { showToast('Duplicate failed: ' + e, 'error'); }
if (btn) btn.disabled = false;
}
async function lbRemoveBg() {
const fname = lbNames[lbIdx];
const btn = document.getElementById('lbNoBgBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Removing…'; }
showToast('Removing background…');
try {
const r = await fetch(`${API}/remove-background/${fname}`, { method: 'POST' });
if (r.ok) {
showToast('Background removed', 'success');
fileHasBg[fname] = false;
_bustStudioImage(fname); // cache-busts viewer + calls updateStudio()
refreshNow(true);
} else {
showToast(`Failed to remove background for ${fname}`, 'error');
}
} catch (e) {
showToast(`Failed to remove background: ${e}`, 'error');
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'No BG'; }
}
}
async function lbInvertAlpha() {
const fname = lbNames[lbIdx];
if (!fname || !/\.(png|webp)$/i.test(fname)) { showToast('Invert needs a transparent PNG', 'info'); return; }
const btn = document.getElementById('lbInvertAlphaBtn');
if (btn) btn.disabled = true;
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/invert-alpha`, { method: 'POST' });
if (r.ok) {
showToast('Transparency inverted', 'success');
fileHasBg[fname] = false;
_bustStudioImage(fname);
refreshNow(true);
} else {
showToast('Invert failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Invert failed: ' + e, 'error'); }
if (btn) btn.disabled = false;
}
async function lbMakeVideo() {
// Use Shift+Click selected refs if any, otherwise all images in the current group.
let filenames;
if (_sbRefIndices.length >= 2) {
filenames = _sbRefIndices.map(i => lbNames[i]).filter(n => n && !isVideo(n));
if (filenames.length < 2) { showToast('Need at least 2 non-video images selected (Shift+Click filmstrip)', 'info'); return; }
} else {
filenames = lbNames.filter(n => n && !isVideo(n));
if (filenames.length < 2) { showToast('Need at least 2 images in this group', 'info'); return; }
}
const btn = document.getElementById('lbMakeVideoBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Creating…'; }
try {
const r = await fetch(`${API}/generate-video`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames, fps: 8, loop: true }),
});
if (!r.ok) { showToast('Video failed: ' + await r.text(), 'error'); return; }
const d = await r.json();
showToast(`Video created: ${d.output}`, 'success');
_followLatestGid = lbCurrentGid;
refreshNow(true);
} catch (e) { showToast('Video error: ' + e, 'error'); }
if (btn) { btn.disabled = false; btn.textContent = 'Make video'; }
}
// ---------------------------------------------------------------------------
// Orbit 3D preview
// ---------------------------------------------------------------------------
let _orbitPollTimer = null;
let _orbitEngine = 'depth'; // 'depth' or 'qwen'
let _orbitFrames = []; // frame URLs for flipper
let _orbitFlipperIdx = 0;
let _orbitFlipperTimer = null;
let _orbitFlipperFps = 1;
let _orbitFlipperRunning = false;
let _orbitCurrentMp4 = null; // last downloaded MP4 rel path
// ---- engine toggle ----
function orbitSetEngine(engine) {
_orbitEngine = engine;
document.getElementById('orbitEngineDepth').classList.toggle('active', engine === 'depth');
document.getElementById('orbitEngineQwen').classList.toggle('active', engine === 'qwen');
document.getElementById('orbitDepthControls').style.display = engine === 'depth' ? 'flex' : 'none';
document.getElementById('orbitQwenControls').classList.toggle('visible', engine === 'qwen');
if (engine === 'qwen') _refreshQwenCacheHint();
}
async function _refreshQwenCacheHint() {
const fname = lbNames[lbIdx];
const cacheEl = document.getElementById('orbitQwenCacheStatus');
const cacheBtn = document.getElementById('orbitLoadCacheBtn');
if (!fname) return;
cacheEl.style.display = 'none';
cacheBtn.style.display = 'none';
try {
const r = await fetch(`${IMAGE_FOLDER}_data/turntables.json?t=` + Date.now());
if (!r.ok) return;
const d = await r.json();
_turntablesData = d.turntables || [];
const found = _turntablesData.find(t =>
t.preferred_filename === fname || lbNames.includes(t.preferred_filename)
);
if (!found) return;
cacheEl.style.display = '';
if (found.completed) {
cacheEl.textContent = '✓ Pre-generated — instant load available';
cacheEl.style.color = '#4ade80';
cacheBtn.style.display = '';
} else {
const pct = Math.round(found.n_done / found.n_total * 100);
cacheEl.textContent = `Background: ${found.n_done}/${found.n_total} views (${pct}%)`;
cacheEl.style.color = '#f59e0b';
}
} catch(_) {}
}
// ---- open modal in settings state ----
function lbOrbit() {
const fname = lbNames[lbIdx];
if (!fname || isVideo(fname)) { showToast('Select an image first', 'info'); return; }
// Always open in settings, never auto-play previous result
_orbitShowSettings();
document.getElementById('orbitModal').classList.add('open');
if (_orbitEngine === 'qwen') _refreshQwenCacheHint();
}
function _orbitShowSettings() {
_orbitFlipperStop();
document.getElementById('orbitSettings').style.display = '';
document.getElementById('orbitPlayer').style.display = 'none';
}
function orbitBackToSettings() {
_orbitShowSettings();
if (_orbitEngine === 'qwen') _refreshQwenCacheHint();
}
// ---- Generate button ----
async function orbitGenerate() {
const fname = lbNames[lbIdx];
if (!fname) return;
// Switch to player state showing "rendering" status
document.getElementById('orbitSettings').style.display = 'none';
document.getElementById('orbitPlayer').style.display = '';
const status = document.getElementById('orbitStatus');
status.className = 'orbit-status';
document.getElementById('orbitFlipperWrap').classList.remove('visible');
document.getElementById('orbitVideo').style.display = 'none';
document.getElementById('orbitDlFramesBtn').style.display = 'none';
document.getElementById('orbitDlBtn').style.display = 'none';
document.getElementById('orbitNobgHint').style.display = 'none';
let body;
if (_orbitEngine === 'qwen') {
const n_views = parseInt(document.getElementById('orbitNviewsSelect').value, 10);
const steps = parseInt(document.getElementById('orbitStepsSelect').value, 10);
body = { filename: fname, engine: 'qwen', n_views, steps };
status.textContent = 'Generating Qwen turntable (~9 min for 24 views)…';
} else {
const parallax = parseInt(document.getElementById('orbitParallaxSlider').value, 10) / 100;
const mode = document.getElementById('orbitModeSelect').value;
body = { filename: fname, engine: 'depth', parallax, mode, n_frames: 36, fps: 24 };
status.textContent = 'Rendering orbit frames…';
}
let jobId;
try {
const r = await fetch(`${API}/orbit`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify(body),
});
if (!r.ok) {
status.textContent = 'Error: ' + await r.text();
status.className = 'orbit-status err';
return;
}
const d = await r.json();
jobId = d.job_id;
if (d.cached) status.textContent = 'Loading cached turntable…';
} catch(e) {
status.textContent = 'Fetch error: ' + e;
status.className = 'orbit-status err';
return;
}
_orbitPoll(jobId);
}
// ---- Load cached instantly ----
async function orbitLoadCache() {
const fname = lbNames[lbIdx];
if (!fname) return;
document.getElementById('orbitSettings').style.display = 'none';
document.getElementById('orbitPlayer').style.display = '';
const status = document.getElementById('orbitStatus');
status.textContent = 'Loading cached turntable…';
status.className = 'orbit-status';
document.getElementById('orbitFlipperWrap').classList.remove('visible');
document.getElementById('orbitNobgHint').style.display = 'none';
try {
const r = await fetch(`${IMAGE_FOLDER}_data/turntables.json?t=` + Date.now());
if (!r.ok) throw new Error('turntables.json not found');
const d = await r.json();
_turntablesData = d.turntables || [];
const found = _turntablesData.find(t =>
t.preferred_filename === fname || lbNames.includes(t.preferred_filename)
);
if (!found || !found.frames || !found.frames.length) {
status.textContent = 'No cached frames found';
status.className = 'orbit-status err';
return;
}
_orbitShowResult({ frames: found.frames, video_filename: found.video_path });
} catch(e) {
status.textContent = 'Error: ' + e;
status.className = 'orbit-status err';
}
}
// ---- Poll job until done ----
function _orbitPoll(jobId) {
if (_orbitPollTimer) clearInterval(_orbitPollTimer);
let dots = 0;
_orbitPollTimer = setInterval(async () => {
dots = (dots + 1) % 4;
try {
const jd = await (await fetch(`${API}/batch/${jobId}`)).json();
const status = document.getElementById('orbitStatus');
if (jd.status === 'running') {
const detail = jd.status_detail ? ` (${jd.status_detail})` : '';
status.textContent = 'Rendering' + '.'.repeat(dots + 1) + detail;
} else if (jd.status === 'done') {
clearInterval(_orbitPollTimer);
_orbitShowResult(jd);
} else {
clearInterval(_orbitPollTimer);
status.textContent = 'Failed: ' + (jd.error || jd.status);
status.className = 'orbit-status err';
}
} catch(_) {}
}, 1500);
}
// ---- Display result ----
function _orbitShowResult(jd) {
const status = document.getElementById('orbitStatus');
const cached = jd.cached ? ' (cached)' : '';
if (jd.frames && jd.frames.length > 0) {
// Frame flipper — shows transparency
_orbitStartFlipper(jd.frames);
status.textContent = `Done${cached}${jd.frames.length} frames`;
status.className = 'orbit-status ok';
document.getElementById('orbitDlFramesBtn').style.display = '';
if (jd.video_filename) {
_orbitCurrentMp4 = jd.video_filename;
document.getElementById('orbitDlBtn').style.display = '';
}
} else if (jd.video_filename) {
// Depth-card MP4 fallback
const mp4 = jd.video_filename;
const src = IMAGE_FOLDER + mp4;
const vid = document.getElementById('orbitVideo');
vid.src = src + '?t=' + Date.now();
vid.style.display = '';
vid.load(); vid.play().catch(()=>{});
status.textContent = `Done${cached}`;
status.className = 'orbit-status ok';
_orbitCurrentMp4 = mp4;
document.getElementById('orbitDlBtn').style.display = '';
if (jd.has_alpha === false)
document.getElementById('orbitNobgHint').style.display = '';
}
_pollTurntableBadge();
}
// ---- Frame flipper engine ----
function _orbitStartFlipper(frameRelPaths) {
_orbitFlipperStop();
_orbitFrames = frameRelPaths.map(p => IMAGE_FOLDER + p);
_orbitFlipperIdx = 0;
_orbitFlipperRunning = true;
const wrap = document.getElementById('orbitFlipperWrap');
wrap.classList.add('visible');
document.getElementById('orbitVideo').style.display = 'none';
document.getElementById('orbitFlipperPlayBtn').textContent = '⏸ Pause';
const img = document.getElementById('orbitFlipperImg');
if (_orbitFrames.length > 0) img.src = _orbitFrames[0];
_orbitFlipperTimer = setInterval(() => {
_orbitFlipperIdx = (_orbitFlipperIdx + 1) % _orbitFrames.length;
document.getElementById('orbitFlipperImg').src = _orbitFrames[_orbitFlipperIdx];
document.getElementById('orbitFrameCounter').textContent =
`${_orbitFlipperIdx + 1}/${_orbitFrames.length}`;
}, Math.round(1000 / _orbitFlipperFps));
}
function _orbitFlipperStop() {
if (_orbitFlipperTimer) { clearInterval(_orbitFlipperTimer); _orbitFlipperTimer = null; }
_orbitFlipperRunning = false;
}
function orbitFlipperToggle() {
if (_orbitFlipperRunning) {
_orbitFlipperStop();
document.getElementById('orbitFlipperPlayBtn').textContent = '▶ Play';
} else {
_orbitStartFlipper(_orbitFrames.map(u => u.startsWith(IMAGE_FOLDER) ? u.slice(IMAGE_FOLDER.length) : u));
document.getElementById('orbitFlipperPlayBtn').textContent = '⏸ Pause';
}
}
function _orbitSetFps(fps) {
_orbitFlipperFps = fps;
if (_orbitFlipperRunning && _orbitFrames.length > 0) {
// Restart with new interval
_orbitFlipperStop();
_orbitStartFlipper(_orbitFrames.map(u => u.startsWith(IMAGE_FOLDER) ? u.slice(IMAGE_FOLDER.length) : u));
}
}
// ---- Download helpers ----
function orbitDownload() {
if (!_orbitCurrentMp4) return;
const a = document.createElement('a');
a.href = IMAGE_FOLDER + _orbitCurrentMp4;
a.download = _orbitCurrentMp4.split('/').pop();
a.click();
}
function orbitDownloadFrames() {
// Download all frames as individual files
if (!_orbitFrames.length) return;
_orbitFrames.forEach((url, i) => {
const a = document.createElement('a');
a.href = url;
a.download = `orbit_frame_${String(i).padStart(3,'0')}.png`;
// Stagger downloads slightly to avoid browser throttle
setTimeout(() => a.click(), i * 80);
});
}
function closeOrbitModal() {
if (_orbitPollTimer) { clearInterval(_orbitPollTimer); _orbitPollTimer = null; }
_orbitFlipperStop();
document.getElementById('orbitModal').classList.remove('open');
const v = document.getElementById('orbitVideo');
v.pause(); v.src = '';
}
// Close on backdrop click
document.getElementById('orbitModal').addEventListener('click', function(e) {
if (e.target === this) closeOrbitModal();
});
// --- Background turntable progress badge + sidebar orbit tab ----------
let _turntablesData = []; // cached from _data/turntables.json
async function _pollTurntableBadge() {
try {
const r = await fetch(`${API}/turntable/status`);
if (!r.ok) return;
const d = await r.json();
const badge = document.getElementById('orbitBadge');
const tbStatus = document.getElementById('turntableBgStatus');
const tbsDot = document.getElementById('tbsDot');
const tbsText = document.getElementById('tbsText');
const total = d.n_total || 0;
const done = d.n_complete || 0;
if (total === 0) { badge.style.display = 'none'; }
else if (done < total) { badge.textContent = `${done}/${total}`; badge.style.display = ''; }
else { badge.style.display = 'none'; }
if (document.getElementById('orbitModal').classList.contains('open') && tbStatus) {
tbStatus.style.display = 'flex';
if (d.is_generating) {
tbsDot.className = 'tbs-dot active';
tbsText.textContent = `Generating — ${done}/${total} actors done`;
} else if (done >= total && total > 0) {
tbsDot.className = 'tbs-dot done';
tbsText.textContent = `All ${total} actors pre-generated`;
} else if (d.is_paused) {
tbsDot.className = 'tbs-dot';
tbsText.textContent = 'Background generation paused';
} else {
tbsDot.className = 'tbs-dot';
tbsText.textContent = `Idle — ${done}/${total} actors have turntables (resumes after 45s idle)`;
}
}
// Refresh orbit tab badge on the tab button
const tabBtn = document.getElementById('sbTabOrbit');
if (tabBtn && done > 0) {
tabBtn.textContent = `Orbit${done < total ? ' (' + done + '/' + total + ')' : ' ✓'}`;
}
} catch(_) {}
}
setInterval(_pollTurntableBadge, 15000);
_pollTurntableBadge();
// ---- Orbit sidebar tab ----
let _orbitTabFlippers = {}; // group_id -> {timer, idx, frames, img}
async function renderSidebarOrbit() {
// Stop any existing full-player and clean up mini-flippers to avoid timer leaks
const oldViewer = document.getElementById('orbitFullViewer');
if (oldViewer && oldViewer._flipperTimer) {
clearTimeout(oldViewer._flipperTimer);
oldViewer._flipperTimer = null;
}
for (const gid in _orbitTabFlippers) {
if (_orbitTabFlippers[gid] && _orbitTabFlippers[gid].timer) {
clearInterval(_orbitTabFlippers[gid].timer);
}
}
_orbitTabFlippers = {};
const panel = document.getElementById('sbPanelOrbit');
const savedScrollTop = panel ? panel.scrollTop : 0;
panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">Loading…</div>';
try {
const r = await fetch(`${IMAGE_FOLDER}_data/turntables.json?t=` + Date.now());
if (!r.ok) { panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>'; return; }
const { turntables } = await r.json();
_turntablesData = turntables || [];
} catch(_) {
panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>';
return;
}
if (!_turntablesData.length) {
panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables generated yet.<br>Background generation starts when the app is idle for 45s.</div>';
return;
}
// Header with controls
let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<span style="font-size:11px;color:#666">${_turntablesData.length} groups</span>
<button class="sb-btn" style="font-size:10px" onclick="renderSidebarOrbit()">↻ Refresh</button>
</div>
<div id="orbitFullViewer" class="orbit-full-player"></div>
<div class="orbit-tab-grid" id="orbitTabGrid">`;
for (const t of _turntablesData) {
const label = t.group_name || t.group_id.slice(0, 8);
const thumb = t.frames.length > 0 ? IMAGE_FOLDER + t.frames[0] : '';
const compClass = t.completed ? ' complete' : '';
html += `<div class="orbit-tab-card${compClass}" onclick="_orbitTabOpen('${t.group_id}')" id="otc_${t.group_id}">
<div class="otc-img-wrap"><img id="otcImg_${t.group_id}" src="${thumb}" alt="${label}"></div>
<div class="otc-name" title="${label}">${label}</div>
<div class="otc-progress">${t.completed ? '✓' : `${t.n_done}/${t.n_total}`}</div>
</div>`;
}
html += '</div>';
panel.innerHTML = html;
// Start mini-flippers for completed turntables
for (const t of _turntablesData) {
if (t.completed && t.frames.length > 1) {
_orbitStartTabFlipper(t.group_id, t.frames);
}
}
if (panel) {
panel.scrollTop = savedScrollTop;
requestAnimationFrame(() => {
if (panel) panel.scrollTop = savedScrollTop;
});
}
}
function _orbitStartTabFlipper(groupId, frameRels) {
const imgEl = document.getElementById(`otcImg_${groupId}`);
if (!imgEl) return;
const urls = frameRels.map(f => IMAGE_FOLDER + f);
let idx = 0;
if (_orbitTabFlippers[groupId]) clearInterval(_orbitTabFlippers[groupId].timer);
const timer = setInterval(() => {
idx = (idx + 1) % urls.length;
const el = document.getElementById(`otcImg_${groupId}`);
if (el) el.src = urls[idx];
}, 1000); // 1fps in thumbnails
_orbitTabFlippers[groupId] = { timer, idx, urls };
}
function _orbitTabOpen(groupId) {
const t = _turntablesData.find(x => x.group_id === groupId);
if (!t || !t.frames.length) return;
const viewer = document.getElementById('orbitFullViewer');
if (!viewer) return;
// Stop any existing full-player
if (viewer._flipperTimer) { clearTimeout(viewer._flipperTimer); viewer._flipperTimer = null; }
const urls = t.frames.map(f => IMAGE_FOLDER + f);
let idx = 0;
const label = t.group_name || t.group_id.slice(0, 8);
viewer.className = 'orbit-full-player visible';
viewer.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;flex-shrink:0">
<span style="font-size:11px;color:#aaa;font-weight:600">${label}</span>
<button class="sb-btn" style="font-size:10px" onclick="
clearTimeout(this.closest('.orbit-full-player')._flipperTimer);
this.closest('.orbit-full-player').classList.remove('visible')">✕</button>
</div>
<div class="orbit-full-img-wrap">
<img id="orbitFullImg" src="${urls[0]}">
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:nowrap;flex-shrink:0">
<label style="font-size:11px;color:#666;display:flex;align-items:center;gap:6px;white-space:nowrap">
Speed
<input type="range" id="orbitFullFpsSlider" min="1" max="30" value="1" step="1"
style="width:60px;accent-color:#f59e0b"
oninput="_orbitFullSetFps(+this.value,this.nextElementSibling)">
<span style="min-width:30px">1fps</span>
</label>
<span id="orbitFullCounter" style="font-size:10px;color:#555;white-space:nowrap">1/${urls.length}</span>
</div>
<div style="margin-top:10px;display:flex;justify-content:flex-end">
<button class="sb-btn" style="font-size:10px;color:#ef4444" onclick="resetOrbit('${groupId}')">Reset / Remove Orbit</button>
</div>`;
let fps = 1;
function tick() {
idx = (idx + 1) % urls.length;
const el = document.getElementById('orbitFullImg');
if (el) el.src = urls[idx];
const ctr = document.getElementById('orbitFullCounter');
if (ctr) ctr.textContent = `${idx+1}/${urls.length}`;
viewer._flipperTimer = setTimeout(tick, Math.round(1000 / fps));
}
window._orbitFullSetFps = (v, label) => { fps = v; if(label) label.textContent = v+'fps'; };
viewer._flipperTimer = setTimeout(tick, Math.round(1000 / fps));
// Scroll into view
viewer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
async function resetOrbit(groupId) {
if (!confirm(`Are you sure you want to completely remove all orbit files and database records for group: ${groupId}?`)) return;
try {
const r = await fetch(`${API}/turntable/${encodeURIComponent(groupId)}`, { method: 'DELETE' });
if (r.ok) {
showToast('Orbit files and DB records removed successfully', 'success');
const viewer = document.getElementById('orbitFullViewer');
if (viewer) {
if (viewer._flipperTimer) {
clearTimeout(viewer._flipperTimer);
viewer._flipperTimer = null;
}
viewer.classList.remove('visible');
viewer.innerHTML = '';
}
setTimeout(refreshNow, 500);
} else {
const text = await r.text();
showToast(`Failed to reset orbit: ${text}`, 'error');
}
} catch (e) {
showToast(`Failed to reset orbit: ${e}`, 'error');
}
}
// Force the studio image (filmstrip + main viewer) to reload from disk.
function _bustStudioImage(fname) {
const t = Date.now();
const i = lbNames.indexOf(fname);
if (i >= 0) lbUrls[i] = IMAGE_FOLDER + fname + '?t=' + t;
const img = document.getElementById('lbImg');
if (img && lbNames[lbIdx] === fname) img.src = IMAGE_FOLDER + fname + '?t=' + t;
updateStudio();
}
function archiveGroup(el, event) {
if (event) event.stopPropagation();
const card = el.closest('.image-card');
card.classList.add('confirming-archive');
}
function cancelArchiveGroup(el, event) {
if (event) event.stopPropagation();
const card = el.closest('.image-card');
card.classList.remove('confirming-archive');
}
async function confirmArchiveGroup(el, event) {
if (event) event.stopPropagation();
const card = el.closest('.image-card');
const gid = card.dataset.group;
// Get all filenames in this group from groupData
const data = groupData.get(gid);
const filenames = data ? data.names : [];
try {
const r = await fetch(`${API}/groups/${gid}/archive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames })
});
if (r.ok) {
const d = await r.json();
showToast(`Group ${gid} archived (${d.count || 0} items)`, 'success');
console.log("[archive] Response:", d);
// Immediately hide the card from UI to feel responsive
card.style.opacity = '0.5';
card.style.pointerEvents = 'none';
setTimeout(refreshNow, 700);
} else {
showToast(`Failed to archive group ${gid}`, 'error');
card.classList.remove('confirming-archive');
}
} catch (e) {
showToast(`Failed to archive group: ${e}`, 'error');
card.classList.remove('confirming-archive');
}
}
async function removeBgGroup(el, event) {
if (event) event.stopPropagation();
const gid = el.closest('.image-card').dataset.group;
showToast(`Removing background for group ${gid}...`);
try {
const r = await fetch(`${API}/remove-background/group/${gid}`, { method: 'POST' });
if (r.ok) {
showToast(`Background removal started for group ${gid}`, 'info');
} else {
showToast(`Failed to start background removal for group ${gid}`, 'error');
}
} catch (e) {
showToast(`Failed to remove group background: ${e}`, 'error');
}
}
async function handleUpload(eventOrFiles) {
const files = eventOrFiles.target ? eventOrFiles.target.files : eventOrFiles;
if (!files || files.length === 0) return;
showToast(`Uploading ${files.length} items...`);
for (const file of files) {
const formData = new FormData();
formData.append('image', file);
// If it's a pasted file, it might not have a useful name
const fileName = file.name || 'clipboard.png';
try {
const r = await fetch(`${API}/upload`, {
method: 'POST',
body: formData
});
if (r.ok) {
showToast(`Upload successful: ${fileName}`);
} else {
showToast(`Upload failed for ${fileName}`, 'error');
}
} catch (e) {
showToast(`Error uploading ${fileName}: ${e}`, 'error');
}
}
// The source image is registered by a background task slightly after /upload
// returns; one debounced refresh picks it up without piling up concurrent loads.
setTimeout(refreshNow, 800);
}
// --- clipboard paste support ---
window.addEventListener('paste', async (e) => {
const items = e.clipboardData.items;
const files = [];
for (const item of items) {
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
files.push(blob);
}
}
if (files.length > 0) {
// If studio is open, offer to add to current group without triggering poses
if (lbCurrentGid) {
showPasteGroupDialog(files);
} else {
handleUpload(files);
}
}
});
function showToast(message, type = 'info', duration = 3000) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => toast.classList.remove('show'), duration);
}
function formatTime(date) {
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return date.toLocaleDateString();
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// Try to get file info using HEAD requests (works with some local servers)
async function getFileInfo(filename) {
try {
const response = await fetch(IMAGE_FOLDER + filename, { method: 'HEAD' });
const lastMod = response.headers.get('last-modified');
const size = response.headers.get('content-length');
return {
modified: lastMod ? new Date(lastMod) : new Date(),
size: size ? parseInt(size) : 0
};
} catch {
return { modified: new Date(), size: 0 };
}
}
async function scanFolder() {
// For local file:// protocol, we can't list directories.
// This works when served via HTTP or you can manually list files.
// METHOD 1: If you have a simple file server, it might return directory listing
try {
const response = await fetch(IMAGE_FOLDER);
const text = await response.text();
// Try to parse common directory listing formats
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const links = Array.from(doc.querySelectorAll('a'))
.map(a => a.getAttribute('href'))
.filter(href => href && EXTENSIONS.some(ext => href.toLowerCase().endsWith(ext)))
.map(href => href.split('/').pop()); // Just the filename
if (links.length > 0) return links;
} catch (e) {
// Directory listing not available
}
// METHOD 2: Manual file list (fallback)
// If you can't use a server, list your files here or use the manual input below
return null;
}
function _hydrateImageMeta(imgs) {
imgs.forEach(img => {
if (img.name) imageNames[img.filename] = img.name;
if (img.clip_description)clipDescriptions[img.filename]= img.clip_description;
if (img.pose) filePoses[img.filename] = img.pose;
if (img.prompt) filePrompts[img.filename] = img.prompt;
if (img.group_name && img.group_id) groupNames[img.group_id] = img.group_name;
fileSortOrders[img.filename] = img.sort_order ?? null;
fileHidden[img.filename] = !!img.hidden;
fileArchived[img.filename] = !!img.archived;
fileTags[img.filename] = img.tags || [];
fileIsSource[img.filename] = !!img.is_source;
fileHasBg[img.filename] = img.has_background !== false;
fileHasClothing[img.filename] = img.has_clothing ?? null;
fileContentType[img.filename] = img.content_type || 'image';
fileFaceswapSrcVideo[img.filename] = img.faceswap_source_video || null;
if (img.source_refs) {
try { fileSourceRefs[img.filename] = JSON.parse(img.source_refs); } catch(e) {}
}
if (img.group_id) customGroups[img.filename] = img.group_id;
});
}
async function loadImages(bypassStatic = false) {
if (loadImages._inFlight) return;
loadImages._inFlight = true;
const gallery = document.getElementById('gallery');
const statusDot = document.getElementById('statusDot');
statusDot.classList.add('updating');
let files = null;
let _staticOk = false;
// Primary: pre-generated static JSON (fast, no DB round-trip per request)
try {
if (bypassStatic) throw new Error("Bypass static requested");
const r = await fetch(`${API}/output/_data/images.json?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) });
if (r.ok) {
const data = await r.json();
let imgs = data.images || [];
_hydrateImageMeta(imgs); // Hydrate everything first!
imgs = _archiveViewActive ? imgs.filter(i => i.archived) : imgs.filter(i => !i.archived);
files = imgs.map(img => img.filename);
_staticOk = true;
}
} catch (e) { /* fall through */ }
// Fallback: live API (handles first startup before static file is written)
if (!files) {
try {
const url = `${API}/images?archived=true`;
const r = await fetch(url, { signal: AbortSignal.timeout(12000) });
if (r.ok) {
const data = await r.json();
_hydrateImageMeta(data.images); // Hydrate everything first!
let imgs = data.images.filter(i => _archiveViewActive ? i.archived : !i.archived);
files = imgs.map(img => img.filename);
}
} catch (e) { /* API not reachable, fall through */ }
}
// Fallback: static list hydrated at page-load time
if (!files && typeof PRELOADED_IMAGES !== 'undefined' && PRELOADED_IMAGES.length > 0) {
files = PRELOADED_IMAGES;
}
if (!files) files = await scanFolder();
if (!files) files = await discoverImages();
const t = Date.now();
const fileObjects = files.map(name => ({
url: IMAGE_FOLDER + name + '?t=' + t,
cleanName: name.split('?')[0]
}));
const newFileNames = new Set(fileObjects.filter(f => !knownFiles.has(f.cleanName)).map(f => f.cleanName));
newFileNames.forEach(n => knownFiles.add(n));
currentFiles = fileObjects;
if (availablePoses && Object.keys(availablePoses).length === 0) {
loadPoses();
}
if (!availableVideos.length) {
loadTemplateVideos();
}
if (fileObjects.length === 0) {
// Only show "no images" if gallery is currently empty (avoid flash on slow API)
if (!gallery.querySelector('.image-card')) {
gallery.innerHTML = `
<div class="empty-state" style="grid-column: 1 / -1;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<h2>No images found</h2>
<p>Place images in the output folder and refresh.</p>
</div>`;
}
statusDot.classList.remove('updating');
loadImages._inFlight = false;
return;
}
// Refresh names/groups/group-names — skip if already hydrated from static JSON
if (!_staticOk) {
try {
const [nr, gr, gnr] = await Promise.all([
fetch(`${API}/names`, { signal: AbortSignal.timeout(2000) }),
fetch(`${API}/groups`, { signal: AbortSignal.timeout(2000) }),
fetch(`${API}/group-names`, { signal: AbortSignal.timeout(2000) }),
]);
if (nr.ok) imageNames = await nr.json();
if (gr.ok) customGroups = await gr.json();
if (gnr.ok) groupNames = await gnr.json();
} catch (e) { /* non-fatal */ }
}
clearCycleTimers();
groupData.clear();
currentGroups = groupFiles(fileObjects);
gallery.innerHTML = currentGroups.map((group, gIdx) => {
const { gid, files: gf } = group;
const urls = gf.map(f => f.url);
const names = gf.map(f => f.cleanName);
// Track which indices are visible (not hidden)
const visibleIdx = names.map((n, i) => i).filter(i => !fileHidden[names[i]]);
groupData.set(gid, { urls, names, visibleIdx });
// Auto-follow: if a generation just finished for this group, jump to the image
if (_followLatestFilename && names.includes(_followLatestFilename) && _isStudioOpen() && lbCurrentGid === gid) {
lbIdx = names.indexOf(_followLatestFilename);
_followLatestFilename = null;
_followLatestGid = null;
lbUrls = urls;
lbNames = names;
updateStudio();
} else if (_followLatestGid === gid && _isStudioOpen() && lbCurrentGid === gid && names.length > 0) {
_followLatestGid = null;
lbIdx = names.length - 1;
lbUrls = urls;
lbNames = names;
updateStudio();
}
const isNew = gf.some(f => newFileNames.has(f.cleanName));
const isRecent = gIdx < 3;
const count = gf.length;
const vCount = visibleIdx.length;
const label = groupDisplayName(group);
const safeGid = gid.replace(/"/g, '&quot;');
const poses = [...new Set(gf.map(f => filePoses[f.cleanName]).filter(Boolean))];
const firstVisUrl = visibleIdx.length ? urls[visibleIdx[0]] : urls[0];
const firstVisName = visibleIdx.length ? names[visibleIdx[0]] : names[0];
const isVideoCard = isVideo(firstVisName) || fileContentType[firstVisName] === 'video';
const isFaceswap = !!fileFaceswapSrcVideo[firstVisName];
const mediaEl = isVideoCard
? `<video src="${firstVisUrl}" poster="${posterFor(firstVisUrl)}" autoplay muted loop playsinline preload="metadata"
onerror="this.style.opacity='0.2'" style="border-radius:0"></video>
<div class="video-play-overlay"><div class="video-play-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="#111">
<polygon points="5,3 19,12 5,21"/>
</svg></div></div>`
: `<img src="${firstVisUrl}" alt="${label}" loading="${isRecent ? 'eager' : 'lazy'}" onerror="this.style.opacity='0.2'">`;
return `
<div class="image-card ${isNew ? 'new' : ''} ${_lastSelectedGid === gid ? 'highlight-prev' : ''}" data-group="${safeGid}" onclick="handleCardClick(this)">
<div class="archive-confirm-overlay" onclick="event.stopPropagation()">
<h3>Archive Group?</h3>
<div class="archive-confirm-buttons">
<button class="btn" style="font-size:11px;padding:4px 8px" onclick="cancelArchiveGroup(this, event)">Cancel</button>
<button class="btn danger" style="font-size:11px;padding:4px 8px" onclick="confirmArchiveGroup(this, event)">Archive</button>
</div>
</div>
<div class="image-actions">
${!isVideoCard ? `<div class="action-btn" title="Remove Background for Group" onclick="removeBgGroup(this, event)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v4m0 12v4M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/>
</svg>
</div>` : ''}
<div class="action-btn delete" title="Archive Entire Group" onclick="archiveGroup(this, event)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 8H3V6h18v2zm-2 1h-14v12h14V9zm-10 3h2v6H9v-6zm4 0h2v6h-2v-6z"/>
</svg>
</div>
</div>
<div class="image-wrapper">
<div class="select-indicator"></div>
${vCount > 1 ? `<div class="variant-count">${vCount}×${count !== vCount ? `<span style="opacity:.5;font-size:9px"> /${count}</span>` : ''}</div>` : ''}
${mediaEl}
${vCount > 1 && !isVideoCard ? `<div class="variant-dots">${visibleIdx.map((_, i) => `<span class="dot${i === 0 ? ' active' : ''}"></span>`).join('')}</div>` : ''}
</div>
<div class="image-info">
<div class="image-name" title="${label}">${label}</div>
<div class="image-meta">
<span class="image-time">#${String(currentGroups.length - gIdx).padStart(3, '0')}</span>
${isVideoCard ? `<span class="badge video" style="font-size:9px">${isFaceswap ? 'Faceswap' : 'Video'}</span>` : ''}
${!isVideoCard && poses.length ? `<span class="badge" style="background:#78350f;font-size:9px">${escHtml(poses[0])}</span>` : ''}
${isRecent && !isVideoCard ? '<span class="badge recent">Latest</span>' : ''}
</div>
</div>
</div>`;
}).join('');
currentGroups.forEach(g => {
const data = groupData.get(g.gid);
if (data && data.visibleIdx && data.visibleIdx.length > 1) startGroupCycle(g.gid);
});
updateCardSelection();
document.getElementById('count').textContent = `${currentGroups.length} group${currentGroups.length !== 1 ? 's' : ''} · ${fileObjects.length} images`;
if (newFileNames.size > 0) showToast(`${newFileNames.size} new image${newFileNames.size !== 1 ? 's' : ''} detected`, 'success');
// If studio is open, sync lbUrls/lbNames from freshly-loaded groupData
const studioEl = document.getElementById('studio');
if (studioEl?.classList.contains('open') && lbCurrentGid) {
const fresh = groupData.get(lbCurrentGid);
if (fresh) {
const curName = lbNames[lbIdx];
const newIdx = fresh.names.indexOf(curName);
// Snapshot ref filenames before overwriting lbNames
const oldRefNames = _sbRefIndices.map(i => lbNames[i]).filter(Boolean);
lbUrls = fresh.urls;
lbNames = fresh.names;
// Keep lbIdx stable when current image not yet in fresh data
// (e.g. duplicate just created — server JSON may lag by the debounce window).
if (newIdx >= 0) {
lbIdx = newIdx;
} else if (lbIdx >= fresh.names.length) {
lbIdx = fresh.names.length - 1;
}
// Re-map multi-ref selections to new positions by filename
_sbRefIndices = oldRefNames
.map(n => fresh.names.indexOf(n))
.filter(i => i >= 0);
updateStudio();
}
}
statusDot.classList.remove('updating');
loadImages._inFlight = false;
}
// Discover images by trying common patterns (fallback for file:// protocol)
async function discoverImages() {
const found = [];
// Try to find images by testing if they exist
// This is a best-effort approach for local file access
// If you know your naming pattern, add it here:
const patterns = [];
// Try to extract from page if there are any references
const images = document.querySelectorAll('img[src]');
images.forEach(img => {
const src = img.getAttribute('src');
if (src && EXTENSIONS.some(ext => src.toLowerCase().includes(ext))) {
found.push(src.split('/').pop().split('?')[0]);
}
});
// Remove duplicates
return [...new Set(found)];
}
function _isStudioOpen() {
return document.getElementById('studio')?.classList.contains('open') ?? false;
}
function setIntervalTime(seconds) {
REFRESH_INTERVAL = seconds;
document.getElementById('statusText').textContent = `Auto-refresh: ${seconds < 60 ? seconds + 's' : (seconds / 60) + 'm'}`;
clearInterval(autoRefreshTimer);
autoRefreshTimer = setInterval(_timedRefresh, REFRESH_INTERVAL * 1000);
showToast(`Refresh interval set to ${seconds < 60 ? seconds + ' seconds' : (seconds / 60) + ' minutes'}`, 'info');
}
// Called by the auto-refresh timer only — skips when studio is open to avoid
// disrupting the user's workflow. Job-completion code calls refreshNow() directly
// and is NOT suppressed (the studio sync path in loadImages handles that safely).
function _timedRefresh() {
if (!_isStudioOpen()) refreshNow();
}
// Debounced refresh — coalesces multiple rapid calls into one loadImages() run.
// Direct callers that need a response (pollJob completion, etc.) still use
// loadImages() directly; everything else should go through refreshNow().
let _refreshTimer = null;
let _refreshBypassStatic = false;
function refreshNow(bypass = false) {
if (bypass) _refreshBypassStatic = true;
clearTimeout(_refreshTimer);
_refreshTimer = setTimeout(() => {
loadImages(_refreshBypassStatic || !!_followLatestGid);
_refreshBypassStatic = false;
}, 80);
}
const API = 'http://127.0.0.1:8500';
// --- selection state ---
let selectionMode = false;
const selectedFiles = new Set();
const selectedVariants = new Map(); // gid -> chosen variant index
const selectedPoses = new Set();
let availablePoses = {};
async function loadPoses() {
try {
const r = await fetch(`${API}/poses`);
if (r.ok) {
availablePoses = await r.json();
renderPoseMenu();
}
} catch (e) { console.error("Failed to load poses", e); }
}
function renderPoseMenu() {
const menu = document.getElementById('poseMenu');
menu.innerHTML = Object.entries(availablePoses).map(([name, entry]) => {
const text = entry?.text ?? entry; // handle legacy string format
const beta = entry?.beta ?? false;
return `<div class="pose-item ${selectedPoses.has(name) ? 'active' : ''}"
onclick="togglePose('${name.replace(/'/g, "\\'")}')" title="${String(text).replace(/"/g, '&quot;')}">
${name}${beta ? ' <span style="font-size:9px;background:#78350f;color:#fbbf24;padding:1px 4px;border-radius:3px;vertical-align:middle">β</span>' : ''}
</div>`;
}).join('');
}
function togglePose(name) {
if (selectedPoses.has(name)) selectedPoses.delete(name);
else selectedPoses.add(name);
renderPoseMenu();
updateBatchBar();
}
function togglePoseMenu() {
document.getElementById('poseMenu').classList.toggle('open');
document.getElementById('poseBtn').classList.toggle('active');
}
// --- variant picker ---
let _pickerGid = null;
function showVariantPicker(gid) {
const data = groupData.get(gid);
if (!data) return;
_pickerGid = gid;
const isSelected = selectedFiles.has(gid);
const chosen = selectedVariants.get(gid);
document.getElementById('variantPickerTitle').textContent =
`Pick reference image — ${data.urls.length} variants`;
document.getElementById('variantPickerGrid').innerHTML = data.urls.map((url, i) => {
const isHidden = fileHidden[data.names[i]];
const isPreferred = i === 0;
const prefClass = isPreferred ? ' preferred' : '';
const prefBtn = !isPreferred
? `<button class="set-preferred-btn" onclick="event.stopPropagation();setAsPreferred('${data.names[i].replace(/'/g,"\\'")}',this)" title="Set as preferred">★</button>`
: '';
return `<div class="variant-thumb${chosen === i ? ' selected' : ''}${isHidden ? ' hidden-img' : ''}${prefClass}"
onclick="selectVariant(${i})" title="${isHidden ? '🚫 Hidden — ' : ''}${isPreferred ? '★ Preferred — ' : ''}Variant ${i + 1}">
<img src="${isVideo(data.names[i]) ? posterFor(url) : url}" loading="lazy">
${prefBtn}
</div>`;
}).join('');
const deselectBtn = document.getElementById('variantPickerDeselect');
deselectBtn.style.display = isSelected ? '' : 'none';
document.getElementById('variantPicker').classList.add('open');
}
function selectVariant(idx) {
const gid = _pickerGid;
if (!gid) return;
selectedFiles.add(gid);
selectedVariants.set(gid, idx);
// Stop cycling and freeze on chosen variant
const timer = cycleTimers.get(gid);
if (timer) { clearInterval(timer); cycleTimers.delete(gid); }
const card = [...document.querySelectorAll('.image-card')].find(c => c.dataset.group === gid);
if (card) {
const data = groupData.get(gid);
if (data) {
const img = card.querySelector('img');
if (img) img.src = data.urls[idx];
card.querySelectorAll('.dot').forEach((d, i) => d.classList.toggle('active', i === idx));
}
}
closeVariantPicker();
updateCardSelection();
updateBatchBar();
}
function deselectGroupFromPicker() {
const gid = _pickerGid;
if (!gid) return;
selectedFiles.delete(gid);
selectedVariants.delete(gid);
_restartCycleIfNeeded(gid);
closeVariantPicker();
updateCardSelection();
updateBatchBar();
}
function closeVariantPicker() {
document.getElementById('variantPicker').classList.remove('open');
_pickerGid = null;
}
function toggleSelectMode() {
selectionMode = !selectionMode;
if (!selectionMode) {
selectedFiles.forEach(gid => _restartCycleIfNeeded(gid));
selectedFiles.clear();
selectedVariants.clear();
}
document.getElementById('selectBtn').classList.toggle('active', selectionMode);
const bar = document.getElementById('batchBar');
bar.classList.toggle('visible', selectionMode);
document.querySelector('.gallery').style.paddingBottom = selectionMode ? '90px' : '40px';
updateCardSelection();
updateBatchBar();
}
function handleCardClick(card) {
const gid = card.dataset.group;
if (selectionMode) {
const data = groupData.get(gid);
if (data && data.urls.length > 1) {
showVariantPicker(gid);
} else {
toggleFile(gid);
}
} else {
openLightbox(gid);
}
}
function toggleFile(base) {
if (selectedFiles.has(base)) {
selectedFiles.delete(base);
selectedVariants.delete(base);
_restartCycleIfNeeded(base);
} else {
selectedFiles.add(base);
}
updateCardSelection();
updateBatchBar();
}
function _restartCycleIfNeeded(gid) {
const data = groupData.get(gid);
if (data && data.visibleIdx && data.visibleIdx.length > 1 && !cycleTimers.has(gid)) {
startGroupCycle(gid);
}
}
function updateCardSelection() {
document.querySelectorAll('.image-card').forEach(card => {
const base = card.dataset.group;
card.classList.toggle('selectable', selectionMode);
card.classList.toggle('selected', selectionMode && selectedFiles.has(base));
});
}
function updateBatchBar() {
const n = selectedFiles.size;
document.getElementById('batchCount').textContent = `${n} group${n !== 1 ? 's' : ''} selected`;
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
const showMR = (n >= 2 && n <= 3);
document.getElementById('multiRefBtn').style.display = showMR ? '' : 'none';
const legend = document.getElementById('multiRefLegend');
if (legend) {
if (showMR) {
const names = [...selectedFiles].map((gid, i) => {
const data = groupData.get(gid);
const idx = selectedVariants.get(gid) ?? 0;
const fname = data?.names[idx] ?? gid;
return `ref${i+1}=${fname.split('/').pop().replace(/\?.*$/,'').slice(0,24)}`;
});
legend.textContent = names.join(' · ');
legend.style.display = '';
} else {
legend.style.display = 'none';
}
}
}
function selectAll() {
currentGroups.forEach(g => selectedFiles.add(g.gid));
updateCardSelection();
updateBatchBar();
}
function deselectAll() {
selectedFiles.forEach(gid => _restartCycleIfNeeded(gid));
selectedFiles.clear();
selectedVariants.clear();
updateCardSelection();
updateBatchBar();
}
// --- batch processing ---
let pollTimer = null;
async function processSelected() {
const prompt = document.getElementById('batchPromptInput').value.trim();
const poseEntries = [...selectedPoses].map(p => ({ name: p, text: availablePoses[p]?.text ?? availablePoses[p] }));
if (!prompt && poseEntries.length === 0) { showToast('Enter a prompt or select a pose', 'info'); return; }
// Build parallel prompt+pose arrays
const prompts = [];
const poses = [];
if (prompt) { prompts.push(prompt); poses.push(null); savePromptHistory(prompt); }
poseEntries.forEach(e => { prompts.push(e.text ?? e); poses.push(e.name); });
// Send the chosen (or newest) file from each selected group
const filenames = [...selectedFiles].flatMap(base => {
const data = groupData.get(base);
if (!data) return [];
const idx = selectedVariants.get(base) ?? 0;
return [data.names[idx]];
});
// Try to use a common group_id if all selected files belong to the same group
let group_id = null;
const groups = new Set();
[...selectedFiles].forEach(base => {
const custom = customGroups[groupData.get(base)?.names[0]];
if (custom) groups.add(custom);
else groups.add(base);
});
if (groups.size === 1) group_id = [...groups][0];
document.getElementById('batchProgress').textContent = 'Submitting…';
try {
const r = await fetch(`${API}/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames, prompt: prompts, poses, seed: -1, max_area: 0, group_id }),
});
if (!r.ok) { showToast('Batch failed to start', 'info'); document.getElementById('batchProgress').textContent = ''; return; }
const { job_id, total } = await r.json();
showToast(`Processing ${total} task${total !== 1 ? 's' : ''}`, 'info');
pollJob(job_id, total);
} catch (e) {
showToast('API not reachable', 'info');
document.getElementById('batchProgress').textContent = '';
}
}
async function processMultiRef() {
const n = selectedFiles.size;
if (n < 2 || n > 3) { showToast('Select 23 images for multi-ref', 'info'); return; }
const prompt = document.getElementById('batchPromptInput').value.trim();
const poseEntries = [...selectedPoses].map(p => ({ name: p, text: availablePoses[p]?.text ?? availablePoses[p] }));
if (!prompt && poseEntries.length === 0) { showToast('Enter a prompt or select a pose', 'info'); return; }
// Resolve chosen variant for each selected group (first selected = primary/image1)
const filenames = [...selectedFiles].map(base => {
const data = groupData.get(base);
if (!data) return null;
const idx = selectedVariants.get(base) ?? 0;
return data.names[idx];
}).filter(Boolean);
const prompts = [];
const poses = [];
if (prompt) { prompts.push(prompt); poses.push(null); savePromptHistory(prompt); }
poseEntries.forEach(e => { prompts.push(e.text); poses.push(e.name); });
// Show which filename maps to which ref slot
const shortName = f => f.split('/').pop().replace(/\?.*$/, '');
let refLabel = `Ref1: ${shortName(filenames[0])} Ref2: ${shortName(filenames[1])}`;
if (filenames[2]) refLabel += ` Ref3: ${shortName(filenames[2])}`;
showToast(refLabel, 'info', 5000);
document.getElementById('batchProgress').textContent = 'Submitting…';
try {
const r = await fetch(`${API}/multi-ref`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames, prompt: prompts, poses, seed: -1, max_area: 0 }),
});
if (!r.ok) { showToast('Multi-ref failed to start', 'info'); document.getElementById('batchProgress').textContent = ''; return; }
const { job_id, total } = await r.json();
showToast(`Multi-ref: ${total} task${total !== 1 ? 's' : ''}`, 'info');
pollJob(job_id, total);
} catch (e) {
showToast('API not reachable', 'info');
document.getElementById('batchProgress').textContent = '';
}
}
function pollJob(job_id, total) {
clearTimeout(pollTimer);
pollTimer = setTimeout(async () => {
try {
const r = await fetch(`${API}/batch/${job_id}`);
if (!r.ok) return;
const job = await r.json();
const done = job.done + job.failed;
document.getElementById('batchProgress').textContent = `${done}/${total}`;
if (job.status === 'running') {
pollJob(job_id, total);
} else {
const msg = job.failed > 0
? `Done: ${job.done} ok, ${job.failed} failed`
: `Done: ${job.done} processed`;
document.getElementById('batchProgress').textContent = msg;
showToast(msg, 'success');
loadImages();
}
} catch (e) { /* ignore transient errors */ pollJob(job_id, total); }
}, 2000);
}
// --- prompt history (localStorage) ---
function loadPromptHistory() {
const hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
const dl = document.getElementById('promptHistory');
dl.innerHTML = hist.map(p => `<option value="${p.replace(/"/g, '&quot;')}"></option>`).join('');
}
function savePromptHistory(prompt) {
let hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
hist = [prompt, ...hist.filter(p => p !== prompt)].slice(0, 20);
localStorage.setItem('batchPromptHistory', JSON.stringify(hist));
loadPromptHistory();
}
// --- tagging ---
let lbCurrentGid = null;
async function tagCurrentImage() {
const data = groupData.get(lbCurrentGid);
if (!data) return;
const filename = data.names[lbIdx];
const btn = document.querySelector('button[onclick="tagCurrentImage()"]');
if (btn) btn.textContent = '…';
try {
const r = await fetch(`${API}/tag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename, threshold: 0.35, max_tags: 8 }),
});
if (r.ok) {
const { name } = await r.json();
imageNames[filename] = name;
document.getElementById('lbName').textContent = name;
showToast(`Tagged: ${name}`, 'success');
// Update the card label too
const card = [...document.querySelectorAll('.image-card')].find(c => c.dataset.group === lbCurrentGid);
if (card) card.querySelector('.image-name').textContent = name;
} else {
const err = await r.text();
showToast(`Tag failed: ${err}`, 'info');
}
} catch (e) { showToast('API not reachable', 'info'); }
if (btn) btn.textContent = 'Re-tag';
}
async function extractCurrentImage() {
const data = groupData.get(lbCurrentGid);
if (!data || data.names.length <= 1) { showToast('Already standalone', 'info'); return; }
const filename = data.names[lbIdx];
try {
const r = await fetch(`${API}/groups/extract`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename }),
});
if (r.ok) {
customGroups[filename] = `solo:${filename}`;
showToast('Extracted to own group', 'success');
closeLightbox();
await loadImages();
}
} catch (e) { showToast('API not reachable', 'info'); }
}
async function groupSelected() {
if (selectedFiles.size < 2) { showToast('Select 2+ groups to merge', 'info'); return; }
// Collect all filenames from selected groups
const filenames = [...selectedFiles].flatMap(gid => {
const data = groupData.get(gid);
return data ? data.names : [];
});
try {
const r = await fetch(`${API}/groups/merge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames }),
});
if (r.ok) {
const { group_id } = await r.json();
filenames.forEach(f => { customGroups[f] = group_id; });
showToast(`Merged into group ${group_id}`, 'success');
deselectAll();
await loadImages();
}
} catch (e) { showToast('API not reachable', 'info'); }
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const img = document.getElementById('lbImg');
if (img) {
const ro = new ResizeObserver(() => {
if (_isStudioOpen()) {
requestAnimationFrame(() => {
updatePadPreview();
const cb = document.getElementById('sbCheckerboard');
if (cb && cb.checked) toggleCheckerboard(true);
});
}
});
ro.observe(img);
}
window.addEventListener('resize', () => {
if (_isStudioOpen()) {
requestAnimationFrame(() => {
updatePadPreview();
const cb = document.getElementById('sbCheckerboard');
if (cb && cb.checked) toggleCheckerboard(true);
});
}
});
loadPromptHistory();
loadImages();
autoRefreshTimer = setInterval(_timedRefresh, REFRESH_INTERVAL * 1000);
// Privacy mode init — apply stored state on load
(function applyPrivacyInit() {
const btn = document.getElementById('privacyBtn');
if (privacyMode) { btn.textContent = '🔒'; document.title = PRIV_TITLE; }
})();
// Studio + variant picker + select-mode keyboard nav
document.addEventListener('keydown', e => {
const tag = document.activeElement && document.activeElement.tagName;
if ((e.key === 'p' || e.key === 'P') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
togglePrivacyMode(); return;
}
if ((e.key === 'c' || e.key === 'C') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) {
const cb = document.getElementById('sbCheckerboard');
const viewer = document.getElementById('studioViewer');
const cur = viewer ? viewer.classList.contains('show-checker') : (cb ? cb.checked : false);
toggleCheckerboard(!cur);
return;
}
}
if ((e.key === 'a' || e.key === 'A') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) {
startManualPad(); return;
}
}
if ((e.key === 'g' || e.key === 'G') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (_activeSidebarTab === 'generate') {
const btn = document.getElementById('sbGenBtn');
if (btn && !btn.disabled) btn.click();
e.preventDefault();
}
}
if (e.altKey && e.key === 'Enter') {
if (document.getElementById('studio').classList.contains('open') && _activeSidebarTab === 'generate') {
const btn = document.getElementById('sbGenBtn');
if (btn && !btn.disabled) {
btn.click();
e.preventDefault();
return;
}
}
}
if (e.key === 'Escape') {
if (document.getElementById('variantPicker').classList.contains('open')) {
closeVariantPicker(); return;
}
if (document.getElementById('studio').classList.contains('open')) {
closeStudio(); return;
}
if (selectionMode) {
toggleSelectMode(); return;
}
}
if (!document.getElementById('studio').classList.contains('open')) return;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
// Throttle filmstrip navigation — ignore if last nav was < 80 ms ago.
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
const now = Date.now();
if (now - (document._lbNavLast || 0) >= 80) {
document._lbNavLast = now;
lbNav(e.key === 'ArrowLeft' ? -1 : 1);
}
e.preventDefault();
}
if (e.key === 'Home') {
lbIdx = 0;
updateStudio();
e.preventDefault();
}
if (e.key === 'End') {
lbIdx = lbUrls.length - 1;
updateStudio();
e.preventDefault();
}
if (e.key === ' ') {
const vid = document.getElementById('lbVideo');
if (vid && vid.style.display !== 'none') {
vid.paused ? vid.play() : vid.pause();
e.preventDefault();
}
}
// Guard async actions against concurrent executions via a simple inflight flag.
if ((e.key === 'h' || e.key === 'H') && tag !== 'INPUT') {
if (!document._lbHideInFlight) { document._lbHideInFlight = true; lbToggleHidden().finally(() => { document._lbHideInFlight = false; }); }
}
if (e.key === 'F' || e.key === 'f') {
lbSetPreferred();
e.preventDefault();
}
if ((e.key === 's' || e.key === 'S') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
toggleSourceKeybind();
e.preventDefault();
}
if ((e.key === 'e' || e.key === 'E') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
estimate21PlusKeybind();
e.preventDefault();
}
if (e.key === 'Delete') {
lbArchive();
e.preventDefault();
}
// ArrowUp/Down move images within the group — debounce rapid keypresses.
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
clearTimeout(document._lbMoveTimer);
const d = e.key === 'ArrowUp' ? -1 : 1;
document._lbMoveTimer = setTimeout(() => lbMoveInGroup(d), 120);
e.preventDefault();
}
// Key 2 moves image to second position in group
if (e.key === '2') {
clearTimeout(document._lbMoveTimer);
document._lbMoveTimer = setTimeout(() => lbMoveToPosition(1), 120); // Position 1 = second place
e.preventDefault();
}
// Key 9 moves image to before the last position in group
if (e.key === '9') {
clearTimeout(document._lbMoveTimer);
document._lbMoveTimer = setTimeout(() => lbMoveToPosition(-2), 120); // Position -2 = second to last
e.preventDefault();
}
});
});
// Privacy overlay — disguise the page as a study-chat on any focus loss.
const PRIV_TITLE = 'AI Assist';
const REAL_TITLE = document.title; // "Studio Monitor"
function showPrivacyOverlay() {
const ov = document.getElementById('privacyOverlay');
ov.style.display = 'flex';
// Force a synchronous layout+style flush so the disguise is painted
// BEFORE the window manager grabs the minimize thumbnail — otherwise
// the first minimize leaks the real content. Text-only overlay = no
// async asset loads to wait on.
void ov.offsetHeight;
document.title = PRIV_TITLE;
}
// blur fires before the minimize snapshot in every browser we target, so
// this is the line of defense for the taskbar thumbnail.
window.addEventListener('blur', () => { if (privacyMode) showPrivacyOverlay(); });
window.addEventListener('pagehide', () => { if (privacyMode) showPrivacyOverlay(); });
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (privacyMode) showPrivacyOverlay();
} else {
// Don't auto-hide — user must click overlay or press P to dismiss
loadImages();
}
});
function togglePrivacyMode() {
privacyMode = !privacyMode;
localStorage.setItem('privacyMode', privacyMode);
const btn = document.getElementById('privacyBtn');
btn.textContent = privacyMode ? '🔒' : '🔓';
if (privacyMode) {
document.title = PRIV_TITLE; // keep the taskbar label discreet even while active
} else {
document.title = REAL_TITLE;
document.getElementById('privacyOverlay').style.display = 'none';
}
}
function disablePrivacyOverlay() {
// Resume work, but privacy mode stays armed — keep the title discreet.
document.getElementById('privacyOverlay').style.display = 'none';
}
// Load the face-book thumbnail for a group. Face extraction runs
// asynchronously after "set preferred", so we ask the API whether the
// crop exists (works over file://, unlike probing with an <img> that
// logs a 404 during the race) and poll a few times while it's pending.
async function loadFaceThumb(gid, retries = 0) {
const faceBook = document.getElementById('lbFaceBook');
const faceThumb = document.getElementById('lbFaceThumb');
if (!faceBook || !faceThumb || !gid) return;
try {
const r = await fetch(`${API}/faces/${encodeURIComponent(gid)}`);
const d = await r.json();
// Ignore stale responses if the user navigated away.
if (lbCurrentGid !== gid || lbIdx !== 0) return;
if (d.exists) {
faceThumb.onerror = () => { faceBook.style.display = 'none'; };
faceThumb.src = IMAGE_FOLDER + d.filename + '?t=' + Date.now();
faceBook.style.display = '';
} else if (retries > 0) {
setTimeout(() => loadFaceThumb(gid, retries - 1), 1500);
} else {
faceBook.style.display = 'none';
}
} catch (e) {
faceBook.style.display = 'none';
}
}
async function setAsPreferred(filename, btn) {
try {
const r = await fetch(`${API}/images/${encodeURIComponent(filename)}/set-preferred`, {method:'POST'});
if (!r.ok) { console.error('set-preferred failed', r.status); return; }
const data = await r.json();
// Update sort orders locally
const gid = data.group_id;
fileSortOrders[filename] = 0;
// Re-open picker to reflect new order
showVariantPicker(filename);
// Poll for the freshly-extracted face crop (async on the server).
loadFaceThumb(gid, 8);
} catch(e) { console.error(e); }
}
async function lbUndress() {
const fname = lbNames[lbIdx];
if (!fname) return;
const btn = document.getElementById('lbUndressBtn');
btn.textContent = '...';
btn.disabled = true;
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/undress`, {method:'POST'});
if (!r.ok) { showToast('Undress failed', 'error'); return; }
showToast('Undress queued', 'success');
} catch(e) {
showToast('Undress error', 'error');
} finally {
btn.textContent = 'Undress';
btn.disabled = false;
}
}
// ---- sidebar & studio ----
function toggleSidebar() {
const sidebar = document.getElementById('studioSidebar');
const btn = document.getElementById('sidebarInnerToggle');
const collapsed = sidebar.classList.toggle('collapsed');
if (btn) btn.textContent = collapsed ? '⊞' : '⊟';
localStorage.setItem('studioSidebarCollapsed', collapsed ? '1' : '0');
}
function switchSidebarTab(tab, initial) {
_tplStopAll(); // release any hovered template video before re-rendering panels
_activeSidebarTab = tab;
localStorage.setItem('studioSidebarTab', tab);
document.querySelectorAll('#studioSidebar .sb-tab').forEach(t =>
t.classList.toggle('active', t.dataset.tab === tab)
);
const panelId = 'sbPanel' + tab.charAt(0).toUpperCase() + tab.slice(1);
document.querySelectorAll('#studioSidebar .sb-panel').forEach(p =>
p.classList.toggle('active', p.id === panelId)
);
if (tab === 'generate') renderSidebarGenerate();
if (tab === 'faceswap') renderSidebarFaceswap();
if (tab === 'scenery') renderSidebarScenery();
if (tab === 'segment') renderSidebarSegment();
if (tab === 'orbit') renderSidebarOrbit();
}
function toggleImageZoom() {
const img = document.getElementById('lbImg');
if (img) {
img.classList.toggle('zoomed');
// The ResizeObserver will handle the follow-up logic automatically.
}
}
// ---- sidebar panels ----
function renderSidebarGenerate() {
const panel = document.getElementById('sbPanelGenerate');
if (!panel) return;
const savedScrollTop = panel.scrollTop;
// Preserve live input values before re-render
const prevPromptVal = document.getElementById('sbGenPromptInput')?.value ?? '';
_sbPadTop = document.getElementById('sbPadTop')?.value ?? _sbPadTop;
_sbPadRight = document.getElementById('sbPadRight')?.value ?? _sbPadRight;
_sbPadBottom = document.getElementById('sbPadBottom')?.value ?? _sbPadBottom;
_sbPadLeft = document.getElementById('sbPadLeft')?.value ?? _sbPadLeft;
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked ?? _sbPadOutpaint;
_sbPadUniform = document.getElementById('sbPadUniform')?.checked ?? _sbPadUniform;
_fsSelectedPoses.forEach(name => {
const el = document.getElementById('sbPoseEdit_' + CSS.escape(name));
if (el) _sbPoseEdits[name] = el.value;
});
const donePoses = new Set();
lbNames.forEach(n => { if (filePoses[n]) donePoses.add(filePoses[n]); });
// Camera angles section — absolute positions
const absAngles = CAMERA_ANGLES.filter(a => !a.relative);
const relAngles = CAMERA_ANGLES.filter(a => a.relative);
const ORBIT_NAMES = ['Front', '¾ Left', '¾ Right', 'Side L', 'Side R', 'Back'];
const allOrbitSel = ORBIT_NAMES.every(n => _sbSelectedAngles.has(n));
let html = `<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px">
<span class="sb-label" style="margin:0;flex:1">Camera — absolute</span>
<button class="sb-btn${allOrbitSel?' primary':''}" style="padding:2px 8px;font-size:10px" onclick="sbToggleOrbitAll()" title="Select all 6 orbit angles (front · ¾ · side · back)">${allOrbitSel?'✓ Orbit all':'Orbit all'}</button>
</div><div class="sb-camera-grid" id="sbCameraGrid">`;
html += absAngles.map(a => {
const sel = _sbSelectedAngles.has(a.name) ? ' selected' : '';
const done = donePoses.has(a.name) ? ' done' : '';
const nSafe = a.name.replace(/'/g, "\\'");
return `<button class="sb-angle-btn${done}${sel}" onclick="toggleSbAngle('${nSafe}')" title="${escHtml(a.prompt)}">${a.icon}<br><span style="font-size:9px">${escHtml(a.name)}</span></button>`;
}).join('');
html += '</div>';
// Relative rotations
html += '<div class="sb-label" style="margin-top:6px">Camera — relative</div><div class="sb-camera-grid" id="sbRelAngleGrid">';
html += relAngles.map(a => {
const sel = _sbSelectedAngles.has(a.name) ? ' selected' : '';
const done = donePoses.has(a.name) ? ' done' : '';
const nSafe = a.name.replace(/'/g, "\\'");
return `<button class="sb-angle-btn${done}${sel}" onclick="toggleSbAngle('${nSafe}')" title="${escHtml(a.prompt)}">${a.icon}<br><span style="font-size:9px">${escHtml(a.name)}</span></button>`;
}).join('');
html += '</div>';
// Wireframe pose reference
const wfSel = _sbWireframeRef || '';
const wfT = _sbWireframeTime;
html += `<div class="sb-sep"></div>
<div class="sb-label">Wireframe pose guide <span style="font-size:10px;color:#555;font-weight:400">(optional)</span></div>
<select id="sbWireframeSelect" onchange="sbSelectWireframe(this.value)"
style="width:100%;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#aaa;font-size:11px;padding:4px;margin-bottom:6px">
<option value="">— none —</option>
${availableVideos.map(v => `<option value="${escHtml(v)}"${wfSel===v?' selected':''}>${escHtml(v.replace(/\.[^.]+$/,''))}</option>`).join('')}
</select>
${wfSel ? `<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
<span style="font-size:11px;color:#777">Frame:</span>
<input type="range" id="sbWireframeTimeSlider" min="0" max="100" value="${Math.round(wfT*100)}"
oninput="sbUpdateWireframeTime(this.value/100)"
style="flex:1">
<span id="sbWireframeTimeLabel" style="font-size:11px;color:#777;min-width:28px">${Math.round(wfT*100)}%</span>
<img id="sbWireframeThumb" src="${API}/wireframe/frame/${encodeURIComponent(wfSel)}?t=${wfT}"
style="width:48px;height:48px;object-fit:contain;border-radius:4px;border:1px solid #333">
</div>` : ''}`;
// Custom prompt field - positioned above poses and under wireframe
html += `<div class="sb-label" style="display:flex;justify-content:space-between;align-items:center">
<span>Custom prompt</span>
<button class="sb-btn" id="sbRefineBtn" style="font-size:9px;padding:2px 6px;margin:0"
onclick="refineSbPrompt()" title="AI prompt refinement">✨ Refine</button>
</div>
<div class="sb-prompt-wrap" style="position:relative;margin-bottom:10px">
<textarea class="sb-input" id="sbGenPromptInput" rows="2" autocomplete="off"
placeholder="Prompt — overrides pose"
oninput="updateSbGenBtn();autoGrowPrompt(this);showPromptSuggest(this)"
onfocus="autoGrowPrompt(this);showPromptSuggest(this)"
onblur="setTimeout(()=>hidePromptSuggest(),150)"
onkeydown="if(event.key==='Escape')hidePromptSuggest()"
style="resize:none;max-height:320px;overflow-y:auto;line-height:1.4;font-family:inherit">${escHtml(prevPromptVal)}</textarea>
<div id="sbPromptSuggest" style="display:none;position:absolute;left:0;right:0;z-index:50;max-height:180px;overflow-y:auto;background:#18181b;border:1px solid #2a2a2a;border-radius:6px;margin-top:2px;box-shadow:0 6px 20px rgba(0,0,0,0.6)"></div>
</div>`;
html += '<div class="sb-sep"></div><div class="sb-label">Poses</div>';
const poseCount = availablePoses ? Object.keys(availablePoses).length : 0;
if (poseCount > 8) {
html += `<input type="text" id="sbPoseFilter" class="sb-input" placeholder="🔍 Filter poses…"
value="${escHtml(_sbPoseFilter)}" oninput="sbFilterPoses(this.value)"
style="margin-bottom:6px;font-size:11px;padding:5px 8px">`;
}
html += '<div class="sb-poses-grid" id="sbPosesGrid">';
if (!poseCount) {
html += '<div style="font-size:11px;color:#555;padding:6px 0">No poses loaded</div>';
} else {
html += Object.entries(availablePoses).map(([name, entry]) => {
let cls = 'sb-pose-btn';
if (donePoses.has(name)) cls += ' done';
if (_fsSelectedPoses.has(name)) cls += ' selected';
if (entry?.beta) cls += ' beta';
const nSafe = name.replace(/'/g, "\\'");
const tip = String(entry?.text ?? entry).replace(/"/g,'&quot;');
return `<button class="${cls}" data-pose-name="${escHtml(name.toLowerCase())}" onclick="toggleSbPose('${nSafe}')" title="${tip}">${escHtml(name)}</button>`;
}).join('');
}
html += `</div>`;
// Fine-tune section: one editable textarea per selected pose.
// "Save" persists the edit to poses.md; "Delete" removes the pose entirely.
if (_fsSelectedPoses.size > 0) {
html += `<div class="sb-sep"></div><div class="sb-label">Fine-tune poses
<span style="font-size:9px;color:#555;font-weight:400">· Save persists to poses.md</span></div>`;
_fsSelectedPoses.forEach(name => {
const origText = String(availablePoses[name]?.text ?? availablePoses[name] ?? name);
const curText = (_sbPoseEdits[name] !== undefined) ? _sbPoseEdits[name] : origText;
const idSafe = escHtml(name).replace(/"/g, '&quot;');
const isBeta = !!(availablePoses[name]?.beta);
html += `<div class="sb-pose-edit" data-pose="${escHtml(name)}">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px">
<span style="font-size:11px;color:#888;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(name)}</span>
<label style="font-size:10px;color:#777;display:flex;align-items:center;gap:3px;cursor:pointer"><input type="checkbox" class="sb-pose-beta" ${isBeta?'checked':''}>beta</label>
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="savePoseToServer(this)">Save</button>
<button class="sb-btn danger" style="padding:2px 8px;font-size:10px" onclick="deletePoseFromServer(this)">Delete</button>
</div>
<textarea id="sbPoseEdit_${idSafe}" class="sb-pose-text"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;resize:vertical;min-height:60px;margin-bottom:6px"
oninput="sbSavePoseEdit('${name.replace(/'/g,"\\'")}')">${escHtml(curText)}</textarea>
</div>`;
});
}
// Add a brand-new pose (persisted to poses.md).
html += `<div class="sb-sep"></div>
<details style="margin-bottom:6px">
<summary style="font-size:11px;color:#888;cursor:pointer">+ New pose</summary>
<input type="text" id="sbNewPoseName" placeholder="Pose name"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;margin:6px 0 4px">
<textarea id="sbNewPoseText" placeholder="Prompt text"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;resize:vertical;min-height:48px"></textarea>
<label style="font-size:10px;color:#777;display:flex;align-items:center;gap:3px;cursor:pointer;margin-top:4px"><input type="checkbox" id="sbNewPoseBeta">beta</label>
<button class="sb-btn primary" style="margin-top:4px;font-size:10px;padding:3px 10px" onclick="addNewPose()">Add pose</button>
</details>`;
// Sticky footer — always visible without scrolling: refs + custom prompt + Generate
html += `<div class="sb-gen-footer">`;
if (_sbRefIndices.length > 0) {
const refThumbs = _sbRefIndices.map((idx, pos) => {
const fn = lbNames[idx] || '';
const u = lbUrls[idx] || '';
const thumb = isVideo(fn) ? posterFor(u) : u;
return `<div class="sb-gen-ref" title="Click to remove · ${escHtml(fn)}" style="cursor:pointer" onclick="sbRemoveRef(${idx})"><img src="${thumb}" loading="lazy" onerror="this.style.opacity='0.3'"><div class="sb-gen-ref-badge">${pos+1}</div><div class="sb-gen-ref-x">×</div></div>`;
}).join('');
html += `<div class="sb-label" style="margin-bottom:4px">References <span style="color:#555;font-weight:400;font-size:9px">· Shift+Click filmstrip or click a thumb to remove</span></div>
<div class="sb-gen-refs">${refThumbs}</div>`;
// With 23 refs, let the user choose how the prompt is applied.
if (_sbRefIndices.length >= 2) {
const n = _sbRefIndices.length;
const modeBtn = (mode, label, tip) =>
`<button onclick="setSbRefMode('${mode}')" title="${tip}"
style="flex:1;padding:5px 6px;font-size:10px;border-radius:5px;cursor:pointer;
border:1px solid ${_sbRefMode===mode?'#2563eb':'#2a2a2a'};
background:${_sbRefMode===mode?'#0c1f44':'#111'};
color:${_sbRefMode===mode?'#7dabff':'#888'}">${label}</button>`;
html += `<div style="display:flex;gap:4px;margin:6px 0 4px">
${modeBtn('combine', 'Combine → 1', 'Blend all selected refs into ONE image (#1 = image1, #2 = image2, #3 = image3)')}
${modeBtn('each', `Each → ${n}`, 'Apply the prompt to each ref separately → one output per ref')}
</div>
<div style="font-size:9px;color:#555;margin-bottom:2px">${_sbRefMode==='combine'
? 'One combined image; reference slots as #1/#2/#3 in the prompt.'
: 'One edit per selected reference.'}</div>`;
}
}
html += `<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:8px">
<span style="font-size:11px;color:#666" title="Expand canvas before generation — gives the model space to outpaint">Pad:</span>
${['Top','Right','Bottom','Left'].map(side => {
const val = side==='Top'?_sbPadTop:side==='Right'?_sbPadRight:side==='Bottom'?_sbPadBottom:_sbPadLeft;
const onInput = `syncPadFields(this)`;
const onBlur = `this.value = formatPadValue(this.value); ${onInput}`;
return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${side[0]}
<input type="text" id="sbPad${side}" value="${val}" placeholder="px or %"
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
oninput="${onInput}" onblur="${onBlur}"
onkeydown="if(event.key === 'Enter') { submitSbGenerate(); event.preventDefault(); }"></label>`;
}).join('')}
<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888;cursor:pointer" title="Keep all padding values equal">
<input type="checkbox" id="sbPadUniform" ${_sbPadUniform?'checked':''} onchange="togglePadUniform(this)" style="cursor:pointer">Uniform</label>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('10%', true)">+10%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('20%', true)">+20%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('50%', true)">+50%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', true)">Reset</button>
<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area">
<input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label>
</div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span id="sbGenJobStatus" style="flex:1;font-size:11px;color:#888;display:none;min-width:120px">
<span class="sb-spinner"></span><span id="sbGenJobText">Generating…</span>
</span>
<button class="sb-btn danger" id="sbCancelBtn" style="display:none" onclick="cancelSbGenerate()">Cancel</button>
<button class="sb-btn primary" id="sbGenBtn" onclick="submitSbGenerate()" disabled>Generate</button>
</div>
</div>`;
panel.innerHTML = html;
updateSbGenBtn();
const promptEl = document.getElementById('sbGenPromptInput');
if (promptEl) autoGrowPrompt(promptEl);
if (_sbPoseFilter) sbFilterPoses(_sbPoseFilter); // re-apply after rebuild
panel.scrollTop = savedScrollTop;
requestAnimationFrame(() => {
if (panel) panel.scrollTop = savedScrollTop;
});
}
// Grow the custom-prompt textarea to fit its content (capped by max-height in CSS).
function autoGrowPrompt(el) {
if (!el) return;
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 160) + 'px';
}
// History autocomplete for the custom-prompt textarea. <datalist> can't bind to a
// <textarea>, so this reuses the same `batchPromptHistory` store that feeds the
// #promptHistory datalist and renders a small suggestion dropdown.
// Prompt history dropdown — shared by the Generate and Scenery prompt textareas.
// Pass the suggestion-box id so each textarea drives its own dropdown.
function showPromptSuggest(el, boxId = 'sbPromptSuggest') {
const box = document.getElementById(boxId);
if (!box) return;
const hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
const q = (el.value || '').trim().toLowerCase();
const matches = (q ? hist.filter(p => p.toLowerCase().includes(q) && p.toLowerCase() !== q) : hist).slice(0, 40);
if (!matches.length) { box.style.display = 'none'; return; }
box.style.top = (el.offsetTop + el.offsetHeight) + 'px';
const inputId = el.id;
box.innerHTML = matches.map(p =>
`<div class="prompt-suggest-item" onmousedown="event.preventDefault();pickPromptSuggest(this,'${inputId}','${boxId}')"
style="padding:6px 9px;font-size:11px;color:#ccc;cursor:pointer;border-bottom:1px solid #222;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
onmouseover="this.style.background='#1f2937'" onmouseout="this.style.background=''"
data-val="${escHtml(p)}" title="${escHtml(p)}">${escHtml(p)}</div>`).join('');
box.style.display = 'block';
}
function hidePromptSuggest(boxId = 'sbPromptSuggest') {
const box = document.getElementById(boxId);
if (box) box.style.display = 'none';
}
function pickPromptSuggest(item, inputId = 'sbGenPromptInput', boxId = 'sbPromptSuggest') {
const el = document.getElementById(inputId);
if (!el) return;
el.value = item.dataset.val;
hidePromptSuggest(boxId);
if (inputId === 'sbGenPromptInput') updateSbGenBtn();
autoGrowPrompt(el);
el.focus();
}
function sbSavePoseEdit(name) {
const el = document.getElementById('sbPoseEdit_' + CSS.escape(name));
if (el) _sbPoseEdits[name] = el.value;
}
// Refetch poses from the backend, then re-render both the Generate tab and batch menu.
async function _reloadPosesUI() {
try {
const r = await fetch(`${API}/poses`);
if (r.ok) availablePoses = await r.json();
} catch (e) { /* non-fatal */ }
renderSidebarGenerate();
if (document.getElementById('poseMenu')) renderPoseMenu();
}
async function savePoseToServer(btn) {
const box = btn.closest('.sb-pose-edit');
if (!box) return;
const name = box.dataset.pose;
const text = box.querySelector('.sb-pose-text')?.value ?? '';
const beta = box.querySelector('.sb-pose-beta')?.checked ?? false;
btn.disabled = true;
try {
const r = await fetch(`${API}/poses`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, text, beta }),
});
if (r.ok) {
delete _sbPoseEdits[name]; // edit is now the saved baseline
showToast('Pose saved', 'success');
await _reloadPosesUI();
} else {
showToast('Save failed: ' + await r.text(), 'error');
btn.disabled = false;
}
} catch (e) { showToast('Save error: ' + e, 'error'); btn.disabled = false; }
}
async function deletePoseFromServer(btn) {
const box = btn.closest('.sb-pose-edit');
if (!box) return;
const name = box.dataset.pose;
if (!confirm(`Delete pose "${name}"?`)) return;
btn.disabled = true;
try {
const r = await fetch(`${API}/poses/${encodeURIComponent(name)}`, { method: 'DELETE' });
if (r.ok) {
_fsSelectedPoses.delete(name);
selectedPoses.delete(name);
delete _sbPoseEdits[name];
showToast('Pose deleted', 'success');
await _reloadPosesUI();
} else {
showToast('Delete failed: ' + await r.text(), 'error');
btn.disabled = false;
}
} catch (e) { showToast('Delete error: ' + e, 'error'); btn.disabled = false; }
}
async function addNewPose() {
const name = (document.getElementById('sbNewPoseName')?.value || '').trim();
const text = (document.getElementById('sbNewPoseText')?.value || '').trim();
const beta = document.getElementById('sbNewPoseBeta')?.checked ?? false;
if (!name) { showToast('Pose name is required', 'info'); return; }
try {
const r = await fetch(`${API}/poses`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, text, beta }),
});
if (r.ok) {
showToast('Pose added', 'success');
await _reloadPosesUI();
} else {
showToast('Add failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Add error: ' + e, 'error'); }
}
function toggleSbPose(name) {
if (_fsSelectedPoses.has(name)) _fsSelectedPoses.delete(name);
else _fsSelectedPoses.add(name);
renderSidebarGenerate();
}
// Live client-side filter of the pose grid by name substring (no re-render).
function sbFilterPoses(q) {
_sbPoseFilter = q || '';
const needle = _sbPoseFilter.trim().toLowerCase();
document.querySelectorAll('#sbPosesGrid .sb-pose-btn').forEach(btn => {
const name = btn.dataset.poseName || '';
btn.style.display = (!needle || name.includes(needle)) ? '' : 'none';
});
}
function toggleSbAngle(name) {
if (_sbSelectedAngles.has(name)) _sbSelectedAngles.delete(name);
else _sbSelectedAngles.add(name);
renderSidebarGenerate();
}
function sbToggleOrbitAll() {
const ORBIT_NAMES = ['Front', '¾ Left', '¾ Right', 'Side L', 'Side R', 'Back'];
const allSel = ORBIT_NAMES.every(n => _sbSelectedAngles.has(n));
if (allSel) { ORBIT_NAMES.forEach(n => _sbSelectedAngles.delete(n)); }
else { ORBIT_NAMES.forEach(n => _sbSelectedAngles.add(n)); }
renderSidebarGenerate();
}
function setSbRefMode(mode) {
_sbRefMode = mode;
renderSidebarGenerate();
}
// Remove a reference by clicking its thumbnail above the custom prompt — no page
// refresh needed (previously refs could only be cleared via Shift+Click filmstrip).
function sbRemoveRef(idx) {
const pos = _sbRefIndices.indexOf(idx);
if (pos >= 0) _sbRefIndices.splice(pos, 1);
updateStudio();
if (_activeSidebarTab === 'generate') renderSidebarGenerate();
}
function sbSelectWireframe(val) {
_sbWireframeRef = val;
renderSidebarGenerate();
}
function sbUpdateWireframeTime(t) {
_sbWireframeTime = t;
const label = document.getElementById('sbWireframeTimeLabel');
if (label) label.textContent = Math.round(t * 100) + '%';
// Debounced thumb update
clearTimeout(sbUpdateWireframeTime._timer);
sbUpdateWireframeTime._timer = setTimeout(() => {
const thumb = document.getElementById('sbWireframeThumb');
if (thumb && _sbWireframeRef) {
thumb.src = `${API}/wireframe/frame/${encodeURIComponent(_sbWireframeRef)}?t=${t}&_=${Date.now()}`;
}
}, 400);
}
async function submitSingleAngle(prompt) {
if (!_fsModelFilename) return;
const bar = document.getElementById('studioAngleBar');
if (bar) bar.style.pointerEvents = 'none';
try {
const r = await fetch(`${API}/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filenames: [_fsModelFilename], prompt: [prompt],
poses: [null], seed: -1, max_area: 0, group_id: lbCurrentGid,
wireframe_ref: _sbWireframeRef || null,
wireframe_time: _sbWireframeTime,
}),
});
if (r.ok) {
const { job_id } = await r.json();
_followLatestGid = lbCurrentGid;
showToast('Generating angle…', 'info');
// Light polling — refresh when done
const poll = () => setTimeout(async () => {
try {
const s = await fetch(`${API}/batch/${job_id}`).then(r => r.json());
if (s.status === 'done') { refreshNow(true); }
else if (s.status !== 'error' && s.status !== 'cancelled') poll();
} catch(e) {}
}, 2000);
poll();
} else { showToast('Angle generation failed', 'error'); }
} catch(e) { showToast('API error: ' + e, 'error'); }
if (bar) bar.style.pointerEvents = '';
}
function updateSbAngleBar() {
const bar = document.getElementById('studioAngleBar');
if (!bar) return;
const isVid = lbNames[lbIdx] && (isVideo(lbNames[lbIdx]) || fileContentType[lbNames[lbIdx]] === 'video');
const hasCrop = !!document.getElementById('cropCanvas');
if (isVid || hasCrop || !lbNames[lbIdx]) {
bar.style.display = 'none';
return;
}
bar.style.display = 'flex';
// Show 6 most useful angles (skip High/Low for overlay — rare)
const overlay = CAMERA_ANGLES.slice(0, 6);
bar.innerHTML = overlay.map(a => {
const pSafe = a.prompt.replace(/'/g, "\\'");
return `<button onclick="submitSingleAngle('${pSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
}).join('') + `
<button onclick="startManualPad()" title="Pad canvas (Expand image)" style="color:#f59e0b">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M12 8v8m-4-4h8"/>
</svg>
</button>`;
}
function updateSbGenBtn() {
const btn = document.getElementById('sbGenBtn');
if (!btn) return;
const hasPrompt = (document.getElementById('sbGenPromptInput')?.value || '').trim().length > 0;
const getV = (id) => document.getElementById(id)?.value || '0';
const padValStr = getV('sbPadTop') + getV('sbPadRight') + getV('sbPadBottom') + getV('sbPadLeft');
const hasPadding = padValStr.replace(/[0%\s]/g, '').length > 0;
const outpaint = document.getElementById('sbPadOutpaint')?.checked;
const hasPaddingAction = hasPadding && outpaint;
const total = _fsSelectedPoses.size + _sbSelectedAngles.size + (hasPrompt ? 1 : 0) + (hasPaddingAction && !hasPrompt ? 1 : 0);
btn.disabled = total === 0;
btn.textContent = total > 1 ? `Generate (${total})` : 'Generate';
}
async function cancelSbGenerate() {
if (!_sbGenJobId) return;
const cancelBtn = document.getElementById('sbCancelBtn');
if (cancelBtn) cancelBtn.disabled = true;
try {
await fetch(`${API}/batch/${_sbGenJobId}`, { method: 'DELETE' });
showToast('Generation cancelled', 'info');
} catch (_) {}
clearTimeout(_sbGenJobPollTimer);
_sbGenJobId = null;
const btn = document.getElementById('sbGenBtn');
const statusEl = document.getElementById('sbGenJobStatus');
if (btn) { btn.disabled = false; btn.textContent = 'Generate'; }
if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.style.display = 'none'; }
if (statusEl) statusEl.style.display = 'none';
}
async function refineSbPrompt() {
const el = document.getElementById('sbGenPromptInput');
if (!el) return;
let prompt = el.value.trim();
if (!prompt) {
// If empty, use current image's prompt if available
if (lbIdx >= 0 && lbNames[lbIdx]) {
const fn = lbNames[lbIdx];
prompt = filePrompts[fn] || '';
}
}
if (!prompt) {
showToast('No prompt to refine. Enter one or select an image.', 'info');
return;
}
const btn = document.getElementById('sbRefineBtn');
const oldHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="sb-spinner" style="width:10px;height:10px;border-width:2px;margin:0"></span> Refining…';
try {
const r = await fetch(`${API}/refine-prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
if (r.ok) {
const { refined } = await r.json();
// Append refined prompt to original
// User said: (original prompt+ gen_poses.py refined prompt)
el.value = (el.value.trim() + "\n\n" + refined).trim();
autoGrowPrompt(el);
updateSbGenBtn();
showToast('Prompt refined', 'success');
} else {
showToast('Refinement failed: ' + await r.text(), 'error');
}
} catch (e) {
showToast('Refinement error: ' + e, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = oldHtml;
}
}
async function submitSbGenerate() {
if (!_fsModelFilename) return;
const promptVal = (document.getElementById('sbGenPromptInput')?.value || '').trim();
const hasPoses = _fsSelectedPoses.size > 0 || _sbSelectedAngles.size > 0;
const getV = (id) => document.getElementById(id)?.value || '0';
const padValStr = getV('sbPadTop') + getV('sbPadRight') + getV('sbPadBottom') + getV('sbPadLeft');
const hasPadding = padValStr.replace(/[0%\s]/g, '').length > 0;
const outpaint = document.getElementById('sbPadOutpaint')?.checked;
const hasPaddingAction = hasPadding && outpaint;
if (!hasPoses && !hasPaddingAction) {
// If no poses or padding, we'll run base_prompts
}
_sbPadTop = document.getElementById('sbPadTop')?.value || '0';
_sbPadRight = document.getElementById('sbPadRight')?.value || '0';
_sbPadBottom = document.getElementById('sbPadBottom')?.value || '0';
_sbPadLeft = document.getElementById('sbPadLeft')?.value || '0';
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked || false;
const prompts = [], poses = [];
_fsSelectedPoses.forEach(name => {
poses.push(name);
// Use edited text if available, else original
const edited = _sbPoseEdits[name];
const orig = availablePoses[name]?.text ?? availablePoses[name] ?? name;
prompts.push((edited !== undefined && edited.trim()) ? edited.trim() : String(orig));
});
// Camera angles — treated as prompts with no pose tag
_sbSelectedAngles.forEach(name => {
const ang = CAMERA_ANGLES.find(a => a.name === name);
// Tag the output with the angle name (as a pose) so the angle button
// shows a green ✓ once generated, mirroring pose tracking.
if (ang) { prompts.push(ang.prompt); poses.push(name); }
});
if (promptVal) { prompts.push(promptVal); poses.push(null); savePromptHistory(promptVal); }
else if (_sbPadOutpaint) {
const isZero = (v) => !v || v === '0' || v === '0%' || v === '0px';
if (!isZero(_sbPadTop) || !isZero(_sbPadRight) || !isZero(_sbPadBottom) || !isZero(_sbPadLeft)) {
// If outpaint is checked but no prompt is given, we still want to trigger a task
prompts.push(""); poses.push(null);
}
}
if (prompts.length === 0) return;
const btn = document.getElementById('sbGenBtn');
btn.disabled = true; btn.textContent = 'Submitting…';
const statusEl = document.getElementById('sbGenJobStatus');
const textEl = document.getElementById('sbGenJobText');
const cancelBtn = document.getElementById('sbCancelBtn');
statusEl.style.display = 'inline-flex';
if (cancelBtn) cancelBtn.style.display = '';
textEl.textContent = `Queuing ${prompts.length} task(s)…`;
// Build filenames array: use multi-ref selection if set, else just primary
const refFilenames = _sbRefIndices.length > 0
? _sbRefIndices.map(idx => lbNames[idx]).filter(Boolean)
: [_fsModelFilename];
// Routing depends on the References mode toggle:
// 'combine' (≥2 refs) → /multi-ref: filenames[0] is primary (image1),
// the rest are extra refs (image2/image3) → ONE output per prompt.
// 'each' or a single ref → /batch: cross-products filenames × prompts
// → one edit per ref. (This was the only path before, which is why
// selecting 3 refs always produced 3 images.)
const useMultiRef = refFilenames.length >= 2 && _sbRefMode === 'combine';
const endpoint = useMultiRef ? '/multi-ref' : '/batch';
const isZero = (v) => !v || v === '0' || v === '0%' || v === '0px';
const padPayload = (!isZero(_sbPadTop) || !isZero(_sbPadRight) || !isZero(_sbPadBottom) || !isZero(_sbPadLeft))
? { pad_top: _sbPadTop, pad_right: _sbPadRight, pad_bottom: _sbPadBottom, pad_left: _sbPadLeft, pad_outpaint: _sbPadOutpaint }
: {};
const payload = useMultiRef
? { filenames: refFilenames, prompt: prompts, poses, seed: -1, max_area: 0, ...padPayload }
: { filenames: refFilenames, prompt: prompts, poses, seed: -1, max_area: 0,
group_id: lbCurrentGid,
wireframe_ref: _sbWireframeRef || null, wireframe_time: _sbWireframeTime,
...padPayload };
if (useMultiRef) {
const shortName = f => f.split('/').pop().replace(/\?.*$/, '');
let refLabel = `Combining → image1: ${shortName(refFilenames[0])}, image2: ${shortName(refFilenames[1])}`;
if (refFilenames[2]) refLabel += `, image3: ${shortName(refFilenames[2])}`;
showToast(refLabel, 'info', 5000);
}
try {
const r = await fetch(`${API}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) {
textEl.textContent = `Error: ${await r.text()}`;
btn.disabled = false; btn.textContent = 'Retry';
if (cancelBtn) cancelBtn.style.display = 'none';
return;
}
const { job_id, total } = await r.json();
_sbGenJobId = job_id;
showToast(`Generating ${total} image${total !== 1 ? 's' : ''}`, 'info');
textEl.textContent = `Generating… 0/${total}`;
let prevDone = 0;
const pollGen = () => {
clearTimeout(_sbGenJobPollTimer);
_sbGenJobPollTimer = setTimeout(async () => {
try {
const jr = await fetch(`${API}/batch/${job_id}`);
if (!jr.ok) { pollGen(); return; }
const job = await jr.json();
const finished = job.done + job.failed;
textEl.textContent = `Generating… ${finished}/${total}`;
// Refresh UI as each task completes; auto-follow to newest image
if (job.done > prevDone) {
prevDone = job.done;
_followLatestGid = lbCurrentGid;
refreshNow(true);
}
if (job.status === 'running') { pollGen(); return; }
if (job.status === 'cancelled') {
statusEl.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'none';
btn.disabled = false; btn.textContent = 'Generate';
_sbGenJobId = null;
return;
}
if (job.status === 'done') {
showToast('Generation complete!', 'success');
statusEl.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'none';
btn.disabled = false; btn.textContent = 'Generate';
_fsSelectedPoses.clear();
_sbSelectedAngles.clear();
_sbPoseEdits = {};
_sbGenJobId = null;
_followLatestGid = lbCurrentGid;
refreshNow(true);
renderSidebarGenerate();
} else {
textEl.textContent = `Error: ${job.error || 'unknown'}`;
btn.disabled = false; btn.textContent = 'Retry';
if (cancelBtn) cancelBtn.style.display = 'none';
_sbGenJobId = null;
}
} catch (e) { pollGen(); }
}, 2000);
};
pollGen();
} catch (e) {
textEl.textContent = `API error: ${e}`;
btn.disabled = false; btn.textContent = 'Retry';
if (cancelBtn) cancelBtn.style.display = 'none';
_sbGenJobId = null;
}
}
// ---- template video cards (faceswap / scenery) ----
// The wireframe library is large (dozens of files, >1 GB total) and lives on a
// network mount. Rendering every card as an autoplaying <video> spun up that many
// simultaneous decoders + range-streams, which OOM-crashed the renderer. Instead
// each card shows a static poster frame (a small server-rendered PNG) and only
// mounts a single real <video> while hovered — at most one decoder alive at a time.
// HTML for one template card. `extraHTML` lets faceswap add its trim button.
function _tplCardHTML(name, selected, onclickExpr, extraHTML = '') {
const stem = name.replace(/\.[^.]+$/, '');
const sel = selected ? ' selected' : '';
const nSafe = name.replace(/'/g, "\\'");
return `<div class="sb-template-card${sel}" onclick="${onclickExpr}"
onmouseenter="_tplPlay(this,'${nSafe}')" onmouseleave="_tplStop(this)">
<img src="${API}/wireframe/frame/${encodeURIComponent(name)}?t=0" loading="lazy" alt="">
<div class="sb-template-label">${escHtml(stem)}</div>${extraHTML}
</div>`;
}
// Mount + play a single <video> over the poster while hovered.
function _tplPlay(card, name) {
if (card._vid) return;
const v = document.createElement('video');
v.src = `${API}/wireframe/${encodeURIComponent(name)}`;
v.muted = true; v.loop = true; v.playsInline = true; v.preload = 'auto';
card.appendChild(v);
card._vid = v;
v.play().catch(() => {});
}
// Tear the video down completely so the decoder + network stream are released.
function _tplStop(card) {
const v = card && card._vid;
if (!v) return;
v.pause();
v.removeAttribute('src');
v.load(); // aborts the in-flight stream and frees the decoder
v.remove();
card._vid = null;
}
// Defensive: stop every active template video in the sidebar (called on tab switch
// so a hovered clip can't keep streaming under a freshly-rendered panel).
function _tplStopAll() {
document.querySelectorAll('#studioSidebar .sb-template-card').forEach(_tplStop);
}
// ---- faceswap sidebar tab ----
async function loadTemplateVideos() {
try {
const r = await fetch(`${API}/output/_data/videos.json`, { signal: AbortSignal.timeout(3000) });
if (r.ok) { availableVideos = (await r.json()).videos || []; return; }
} catch (_) {}
try {
const r = await fetch(`${API}/videos`);
if (!r.ok) return;
availableVideos = (await r.json()).videos || [];
} catch (_) { availableVideos = []; }
}
async function lbFaceswap() {
const data = groupData.get(lbCurrentGid);
if (!data) return;
_fsModelFilename = data.names[lbIdx];
_fsSelectedVideo = null;
if (!availableVideos.length) await loadTemplateVideos();
switchSidebarTab('faceswap');
// Expand sidebar if collapsed
document.getElementById('studioSidebar')?.classList.remove('collapsed');
document.getElementById('sidebarInnerToggle').textContent = '⊟';
}
function renderSidebarFaceswap() {
const panel = document.getElementById('sbPanelFaceswap');
if (!panel) return;
const savedScrollTop = panel.scrollTop;
if (_fsTrimVideo) {
// Clip timeline view
const vSafe = _fsTrimVideo.replace(/'/g, "\\'");
panel.innerHTML = `
<div style="font-size:11px;color:#aaa;margin-bottom:6px;word-break:break-all">✂ ${escHtml(_fsTrimVideo)}</div>
<div class="clt-wrapper">
<video class="clt-video-preview" id="cltVideoPreview" muted preload="metadata"
src="${API}/wireframe/${encodeURIComponent(_fsTrimVideo)}"></video>
<div class="clt-track" id="cltTrack">
<div class="clt-bg"></div>
<div class="clt-range" id="cltRange"></div>
<div class="clt-handle" id="cltHandleStart"></div>
<div class="clt-handle end" id="cltHandleEnd"></div>
<div class="clt-playhead" id="cltPlayhead"></div>
</div>
<div class="clt-labels">
<span id="cltStartLabel">0:00</span>
<span id="cltEndLabel">0:00</span>
</div>
<input type="text" class="sb-input" id="cltOutputName"
placeholder="Output name (auto)" style="margin:6px 0">
<div id="cltStatus" style="font-size:11px;color:#aaa;min-height:16px"></div>
<div style="display:flex;gap:6px;margin-top:8px">
<button class="sb-btn" onclick="closeTrimPanel()">← Back</button>
<button class="sb-btn" onclick="cltPreviewRange()">▶ Preview</button>
<button class="sb-btn primary" onclick="submitTrimFromTimeline()">✂ Create clip</button>
</div>
</div>`;
requestAnimationFrame(() => initClipTimeline());
return;
}
// Template grid view
let html = '<div class="sb-label">Template videos</div>';
if (!availableVideos.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>';
}
html += `<div class="sb-sep"></div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:10px">
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer">
<input type="checkbox" id="sbFsEnhance" checked>
<span>Enhance faces</span>
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer" title="Requires install_facefusion.sh">
<input type="checkbox" id="sbFsHair" disabled>
<span>FaceFusion</span>
<span id="sbFsHairBadge" style="font-size:10px;color:#f87;background:#2a1a1a;padding:1px 5px;border-radius:3px">not installed</span>
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer" title="Generate at lower resolution for quick preview">
<input type="checkbox" id="sbFsPreview" onchange="document.getElementById('sbFsPreviewOpts').style.display=this.checked?'flex':'none'">
<span>Preview mode</span>
</label>
<div id="sbFsPreviewOpts" style="display:none;align-items:center;gap:6px;padding-left:20px">
<span style="font-size:11px;color:#888">Scale:</span>
<select id="sbFsPreviewScale" style="font-size:11px;background:#1a1a1a;color:#ccc;border:1px solid #333;border-radius:3px;padding:1px 4px">
<option value="0.5">50%</option>
<option value="0.25">25%</option>
</select>
</div>
</div>
<div style="display:flex;align-items:center;gap:8px">
<span id="sbFsJobStatus" style="flex:1;font-size:11px;color:#888;display:none">
<span class="sb-spinner"></span><span id="sbFsJobText">Processing…</span>
</span>
<button class="sb-btn primary" id="sbFsStartBtn" onclick="submitFaceswap()"
${_fsSelectedVideo ? '' : 'disabled'}>Start Faceswap</button>
</div>`;
panel.innerHTML = html;
// Cards show static poster frames; videos mount only on hover (_tplPlay).
// Check FaceFusion availability
fetch(`${API}/faceswap/check`).then(r => r.json()).then(s => {
const hairCb = document.getElementById('sbFsHair');
const hairBdg = document.getElementById('sbFsHairBadge');
if (!hairCb) return;
if (s.facefusion) { hairCb.disabled = false; hairBdg.style.display = 'none'; }
else { hairCb.disabled = true; hairBdg.style.display = ''; }
}).catch(()=>{});
panel.scrollTop = savedScrollTop;
requestAnimationFrame(() => {
if (panel) panel.scrollTop = savedScrollTop;
});
}
function sbSelectTemplate(videoName) {
_fsSelectedVideo = videoName;
renderSidebarFaceswap();
}
function openTrimPanel(videoName) {
_fsTrimVideo = videoName;
renderSidebarFaceswap();
}
function closeTrimPanel() {
_fsTrimVideo = null;
renderSidebarFaceswap();
}
async function submitFaceswap() {
if (!_fsModelFilename || !_fsSelectedVideo) return;
const enhance = document.getElementById('sbFsEnhance')?.checked ?? true;
const hairEl = document.getElementById('sbFsHair');
const hair = (hairEl?.checked && !hairEl?.disabled) ?? false;
const previewCb = document.getElementById('sbFsPreview');
const previewScale = (previewCb?.checked)
? parseFloat(document.getElementById('sbFsPreviewScale')?.value ?? '0.5')
: 1.0;
const btn = document.getElementById('sbFsStartBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Starting…'; }
const statusEl = document.getElementById('sbFsJobStatus');
const textEl = document.getElementById('sbFsJobText');
if (statusEl) statusEl.style.display = 'inline-flex';
const prevTag = previewScale < 1.0 ? ` preview ${Math.round(previewScale * 100)}%` : '';
const modeLabel = (hair ? 'FaceFusion + hair' : (enhance ? 'insightface + enhance' : 'insightface')) + prevTag;
if (textEl) textEl.textContent = `Starting (${modeLabel})…`;
try {
const r = await fetch(`${API}/faceswap`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_filename: _fsModelFilename, video_name: _fsSelectedVideo, enhance, hair, preview_scale: previewScale }),
});
if (!r.ok) {
if (textEl) textEl.textContent = `Error: ${await r.text()}`;
if (btn) { btn.disabled = false; btn.textContent = 'Start Faceswap'; }
return;
}
const { job_id } = await r.json();
_fsJobId = job_id;
showToast(`Faceswap started (${_fsSelectedVideo})`, 'info');
pollFaceswapJob(job_id);
} catch (e) {
if (textEl) textEl.textContent = `API error: ${e}`;
if (btn) { btn.disabled = false; btn.textContent = 'Start Faceswap'; }
}
}
function pollFaceswapJob(job_id) {
clearTimeout(_fsJobPollTimer);
_fsJobPollTimer = setTimeout(async () => {
try {
const r = await fetch(`${API}/batch/${job_id}`);
if (!r.ok) { pollFaceswapJob(job_id); return; }
const job = await r.json();
const pct = job.total > 1 ? ` ${job.done}/${job.total} frames` : '';
const textEl = document.getElementById('sbFsJobText');
const btn = document.getElementById('sbFsStartBtn');
if (job.status === 'running') {
if (textEl) textEl.textContent = `Processing${pct}`;
pollFaceswapJob(job_id);
} else if (job.status === 'done') {
if (textEl) textEl.textContent = `Done! ${job.output || ''}`;
if (btn) btn.textContent = 'Done ✓';
showToast(`Faceswap complete: ${job.output || ''}`, 'success');
_followLatestGid = lbCurrentGid;
refreshNow(true);
} else {
if (textEl) textEl.textContent = `Error: ${job.error || 'unknown'}`;
if (btn) { btn.disabled = false; btn.textContent = 'Retry'; }
showToast(`Faceswap failed: ${job.error || ''}`, 'error');
}
} catch (e) { pollFaceswapJob(job_id); }
}, 2500);
}
// ---- clip timeline ----
let _cltDuration = 0, _cltStart = 0, _cltEnd = 0, _cltDragging = null;
function initClipTimeline() {
const video = document.getElementById('cltVideoPreview');
const track = document.getElementById('cltTrack');
if (!video || !track) return;
video.addEventListener('loadedmetadata', () => {
_cltDuration = video.duration || 10;
_cltStart = 0;
_cltEnd = _cltDuration;
updateCltUI();
});
video.addEventListener('timeupdate', () => {
if (_cltDragging || _cltDuration <= 0) return;
const ph = document.getElementById('cltPlayhead');
if (ph) ph.style.left = (video.currentTime / _cltDuration * 100) + '%';
});
track.addEventListener('pointerdown', e => {
const rect = track.getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const startPct = _cltDuration > 0 ? _cltStart / _cltDuration : 0;
const endPct = _cltDuration > 0 ? _cltEnd / _cltDuration : 1;
if (e.target.id === 'cltHandleStart' || (e.target.id !== 'cltHandleEnd' && Math.abs(pct - startPct) <= Math.abs(pct - endPct))) {
_cltDragging = 'start';
} else if (e.target.id === 'cltHandleEnd' || Math.abs(pct - endPct) < Math.abs(pct - startPct)) {
_cltDragging = 'end';
} else {
if (video) video.currentTime = pct * _cltDuration;
return;
}
track.setPointerCapture(e.pointerId);
e.preventDefault();
});
track.addEventListener('pointermove', e => {
if (!_cltDragging || _cltDuration <= 0) return;
const rect = track.getBoundingClientRect();
const t = Math.max(0, Math.min(_cltDuration, (e.clientX - rect.left) / rect.width * _cltDuration));
if (_cltDragging === 'start') _cltStart = Math.min(t, _cltEnd - 0.1);
else _cltEnd = Math.max(t, _cltStart + 0.1);
updateCltUI();
if (video) video.currentTime = _cltDragging === 'start' ? _cltStart : _cltEnd;
});
track.addEventListener('pointerup', e => {
_cltDragging = null;
track.releasePointerCapture(e.pointerId);
});
}
function updateCltUI() {
if (_cltDuration <= 0) return;
const sp = (_cltStart / _cltDuration) * 100;
const ep = (_cltEnd / _cltDuration) * 100;
const sh = document.getElementById('cltHandleStart');
const eh = document.getElementById('cltHandleEnd');
const rn = document.getElementById('cltRange');
const sl = document.getElementById('cltStartLabel');
const el = document.getElementById('cltEndLabel');
if (sh) sh.style.left = sp + '%';
if (eh) eh.style.left = ep + '%';
if (rn) { rn.style.left = sp + '%'; rn.style.width = (ep - sp) + '%'; }
if (sl) sl.textContent = formatSecs(_cltStart);
if (el) el.textContent = formatSecs(_cltEnd);
}
function formatSecs(s) {
const m = Math.floor(s / 60), sec = (s % 60).toFixed(1);
return m + ':' + (parseFloat(sec) < 10 ? '0' : '') + sec;
}
function cltPreviewRange() {
const video = document.getElementById('cltVideoPreview');
if (!video || _cltDuration <= 0) return;
video.currentTime = _cltStart;
video.play();
const stop = () => { if (video.currentTime >= _cltEnd) { video.pause(); video.currentTime = _cltStart; } };
video.addEventListener('timeupdate', stop);
setTimeout(() => video.removeEventListener('timeupdate', stop), (_cltEnd - _cltStart + 2) * 1000);
}
async function submitTrimFromTimeline() {
if (!_fsTrimVideo) return;
const name = (document.getElementById('cltOutputName')?.value || '').trim() || null;
const statusEl = document.getElementById('cltStatus');
if (_cltEnd <= _cltStart) { if (statusEl) statusEl.textContent = 'End must be > start'; return; }
if (statusEl) statusEl.textContent = 'Trimming…';
try {
const r = await fetch(`${API}/wireframe/trim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_name: _fsTrimVideo, start: _cltStart, end: _cltEnd, output_name: name }),
});
if (!r.ok) { if (statusEl) statusEl.textContent = 'Error: ' + await r.text(); return; }
const d = await r.json();
showToast('Clip saved: ' + d.output_name, 'success');
availableVideos = [];
await loadTemplateVideos();
closeTrimPanel();
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
}
// ---- scenery sidebar tab ----
let _sceneVideo = null, _sceneDuration = 0, _sceneFrameBytes = null, _sceneJobPollTimer = null;
let _sceneGridOpen = false; // whether the video picker grid is expanded
let _sceneLibOpen = false; // whether "previous scenery" library panel is expanded
let _sceneLib = null; // cached { groups, ungrouped } from /scenery/library
// 3 positional reference slots: 0=Background(image1), 1=Person(image2), 2=Extra(image3).
// Each entry is null | {kind:'bytes', data:<base64>} | {kind:'file', name:<filename>}.
// Backend contract: slot0→scene_bytes (bytes; a file here is fetched→bytes on submit),
// slot1→model_filename (must be a file), slot2→extra_filename (file, optional).
let _sceneRefs = [null, null, null];
let _scenePersonInit = false; // person auto-filled into slot1 once per group
let _sceneDrag = null; // active drag payload: {type:'slot',idx} | {type:'file',name}
function renderSidebarScenery() {
const panel = document.getElementById('sbPanelScenery');
if (!panel) return;
const savedScrollTop = panel.scrollTop;
// Preserve a typed prompt across the frequent re-renders (slot changes).
const prevScenePrompt = document.getElementById('scenePromptInput')?.value || '';
// Auto-fill the Person slot (image2) once per group with the current image.
const personName = _fsModelFilename || lbNames[lbIdx] || '';
if (!_scenePersonInit && !_sceneRefs[1] && personName) {
_sceneRefs[1] = { kind: 'file', name: personName };
_scenePersonInit = true;
}
const bgRef = _sceneRefs[0];
const bgBytes = (bgRef && bgRef.kind === 'bytes') ? bgRef.data : null;
const haveRef = _sceneVideo || bgBytes;
// Auto-open the grid when no background source is chosen yet.
const gridOpen = _sceneGridOpen || !haveRef;
// --- Interactive 3-slot reference row (drag to reorder, ✕ to clear) ---
const SLOT_META = [
{ n: 1, label: 'Background' },
{ n: 2, label: 'Person' },
{ n: 3, label: 'Extra' },
];
const slotHtml = SLOT_META.map((m, i) => {
const ref = _sceneRefs[i];
const thumb = _sceneThumb(ref);
const kind = ref ? (ref.kind === 'bytes' ? '📷' : '🖼') : '';
return `<div class="scene-slot ${ref ? 'filled' : 'empty'}" data-slot="${i}"
draggable="${ref ? 'true' : 'false'}"
ondragstart="sceneDragSlotStart(event,${i})"
ondragover="sceneDragOver(event)" ondragleave="sceneDragLeave(event)" ondrop="sceneDrop(event,${i})">
<div class="scene-slot-thumb">${thumb
? `<img src="${thumb}" onerror="this.style.opacity=.3">`
: `<span class="ph">${m.n}</span>`}
${ref ? `<button class="scene-slot-x" title="Remove" onclick="sceneClearSlot(${i})">✕</button>` : ''}
${kind ? `<span class="scene-slot-kind">${kind}</span>` : ''}</div>
<div class="scene-slot-cap"><b>Image ${m.n}</b>${m.label}</div>
</div>`;
}).join('');
let html = `<div class="sb-label">References <span style="color:#555;font-weight:400;font-size:9px">· Shift+Click filmstrip (fills →) · drag to reorder</span></div>
<div class="scene-slots">${slotHtml}</div>`;
// --- Image 1 source: background scene (video scrub + capture, or upload) ---
html += `<div class="sb-label">Image 1 source · background</div>`;
// --- Selected video: player + slider + capture (always at top) ---
if (_sceneVideo) {
html += `
<div class="scene-frame-preview" id="sceneFramePreview" style="display:block">
<video id="sceneVideoEl" muted preload="auto" playsinline
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;height:100%;object-fit:contain"></video>
<img id="sceneFrameImg" style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;height:100%;object-fit:contain"
${_sceneFrameBytes?`src="data:image/png;base64,${_sceneFrameBytes}"`:''}/>
<div class="scene-captured-badge" id="sceneCapturedBadge" style="${_sceneFrameBytes?'display:block':'display:none'}">✓ frame captured</div>
</div>
<input type="range" class="scene-scrubber" id="sceneScrubber" min="0" max="1000" value="0"
oninput="sceneScrub(this.value)" ${_sceneFrameBytes?'disabled':''}>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span id="sceneTimeLabel" style="font-size:11px;color:#888">0:00.0</span>
<span style="font-size:10px;color:#555">${escHtml(_sceneVideo.replace(/\.[^.]+$/,''))}</span>
</div>
<button class="sb-btn primary" id="sceneCaptureBtn" onclick="sceneExtractFrame()"
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;margin-bottom:8px">📷 Capture this frame</button>
<button class="sb-btn" id="sceneReleaseBtn" onclick="sceneReleaseFrame()"
style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;margin-bottom:8px">✖ Pick a different frame</button>
<button class="sb-btn" onclick="sceneToggleGrid()" style="width:100%;margin-bottom:10px">
${gridOpen?'▲ Hide video list':'▼ Change background video'}</button>`;
}
// --- Video picker grid (collapsible) ---
if (!_sceneVideo || gridOpen) {
html += '<div class="sb-label">Background video</div>';
if (availableVideos.length) {
html += '<div class="sb-template-grid">';
availableVideos.forEach(v => {
const vSafe = v.replace(/'/g, "\\'");
html += _tplCardHTML(v, _sceneVideo === v, `sceneSelectVideo('${vSafe}')`);
});
html += '</div>';
} else {
html += '<div style="font-size:11px;color:#555;padding:6px 0">No wireframe videos found</div>';
}
}
// Upload alternative for the background (still image 1)
html += `<input type="file" id="sceneUploadInput" accept="image/*" style="display:none" onchange="sceneHandleUpload(event)">
<button class="sb-btn" onclick="document.getElementById('sceneUploadInput').click()" style="width:100%;margin-bottom:10px">⬆ Upload image as background</button>`;
// --- Person / Extra: filled via filmstrip Shift+Click (see slots above) ---
const person = _sceneRefs[1];
html += `<div class="sb-sep"></div>
<div style="font-size:10px;color:#666;line-height:1.5;margin-bottom:8px">
<b style="color:#999">Image 2 (Person)</b> &amp; <b style="color:#999">Image 3 (Extra)</b>:
Shift+Click a thumbnail in the bottom filmstrip to add it (fills the first open
slot right→left). Drag the slots above to rearrange, or ✕ to clear.
</div>`;
if (!person || person.kind !== 'file') {
html += `<div style="font-size:11px;color:#a55;padding:0 0 8px">Image 2 needs a gallery image (Shift+Click the filmstrip).</div>`;
}
// --- Prompt + generate ---
const canGen = !!bgRef && !!person && person.kind === 'file'
&& (!_sceneRefs[2] || _sceneRefs[2].kind === 'file');
html += `<div class="sb-sep"></div>
<div class="sb-label">Prompt (optional) <span style="color:#555;font-weight:400;font-size:9px">· history while typing</span></div>
<div class="sb-prompt-wrap" style="position:relative;margin-bottom:10px">
<textarea class="sb-input" id="scenePromptInput" rows="2" autocomplete="off"
placeholder="Auto: place person in scene… (type to search history)"
oninput="autoGrowPrompt(this);showPromptSuggest(this,'scenePromptSuggest')"
onfocus="autoGrowPrompt(this);showPromptSuggest(this,'scenePromptSuggest')"
onblur="setTimeout(()=>hidePromptSuggest('scenePromptSuggest'),150)"
onkeydown="if(event.key==='Escape')hidePromptSuggest('scenePromptSuggest')"
style="resize:none;max-height:300px;overflow-y:auto;line-height:1.4;font-family:inherit"></textarea>
<div id="scenePromptSuggest" style="display:none;position:absolute;left:0;right:0;z-index:50;max-height:240px;overflow-y:auto;background:#18181b;border:1px solid #2a2a2a;border-radius:6px;margin-top:2px;box-shadow:0 6px 20px rgba(0,0,0,0.6)"></div>
</div>
<div style="display:flex;align-items:center;gap:8px">
<span id="sceneJobStatus" style="flex:1;font-size:11px;color:#888;display:none">
<span class="sb-spinner"></span><span id="sceneJobText">Generating…</span>
</span>
<button class="sb-btn primary" id="sceneGenBtn" onclick="submitGenerateScenery()"
${canGen?'':'disabled'}>Create Scenery</button>
</div>`;
// --- Previous scenery library (grouped by source video) ---
html += `<div class="sb-sep"></div>
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
<span class="sb-label" style="margin:0;flex:1">Previous scenery</span>
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="sceneLibToggle()">${_sceneLibOpen ? '▲ hide' : '▼ browse'}</button>
</div>`;
if (_sceneLibOpen) {
if (!_sceneLib) {
html += `<div id="sceneLibLoader" style="font-size:11px;color:#666;padding:4px 0">Loading…</div>`;
} else if (!_sceneLib.groups.length && !_sceneLib.ungrouped.length) {
html += `<div style="font-size:11px;color:#555;padding:4px 0">No previous scenery found.</div>`;
} else {
const renderLibItems = (items) => items.map(it => {
const url = IMAGE_FOLDER + encodeURIComponent(it.filename);
const gLabel = it.group_id ? ` <span style="color:#555">${escHtml(it.group_id)}</span>` : '';
return `<div style="display:inline-block;margin:2px;cursor:pointer;position:relative;vertical-align:top"
title="Use as background · ${escHtml(it.filename)}${it.group_id?' ('+escHtml(it.group_id)+')':''}"
onclick="sceneLibUseAsBackground(${JSON.stringify(it.filename)})">
<img src="${url}" style="width:52px;height:52px;object-fit:cover;border-radius:4px;border:1px solid #333">
</div>`;
}).join('');
_sceneLib.groups.forEach(g => {
const vName = g.video.replace(/\.[^.]+$/, '');
html += `<div style="margin-bottom:8px">
<div style="font-size:10px;color:#888;margin-bottom:3px">📹 ${escHtml(vName)}</div>
<div>${renderLibItems(g.items)}</div>
</div>`;
});
if (_sceneLib.ungrouped.length) {
html += `<div style="margin-bottom:8px">
<div style="font-size:10px;color:#888;margin-bottom:3px">📷 uploaded / other</div>
<div>${renderLibItems(_sceneLib.ungrouped)}</div>
</div>`;
}
}
}
panel.innerHTML = html;
// Lazy-load library data when panel is opened
if (_sceneLibOpen && !_sceneLib) {
fetch(`${API}/scenery/library`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (d) { _sceneLib = d; renderSidebarScenery(); } })
.catch(() => {});
}
// Restore the preserved prompt text and size the textarea.
const spEl = document.getElementById('scenePromptInput');
if (spEl) { spEl.value = prevScenePrompt; autoGrowPrompt(spEl); }
if (_sceneVideo) {
const vid = document.getElementById('sceneVideoEl');
vid.src = `${API}/wireframe/${encodeURIComponent(_sceneVideo)}`;
// Once metadata is known, size the slider so 1000 steps span the whole clip.
vid.addEventListener('loadedmetadata', () => {
_sceneDuration = vid.duration || 0;
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
}, { once: true });
// Keep the time label in sync if the frame is seeked any other way.
vid.addEventListener('seeked', () => {
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
});
}
panel.scrollTop = savedScrollTop;
requestAnimationFrame(() => {
if (panel) panel.scrollTop = savedScrollTop;
});
}
// Scrub the preview video live as the slider moves (no server round-trip).
function sceneScrub(val) {
const vid = document.getElementById('sceneVideoEl');
if (!vid || !vid.duration) return;
const t = (parseFloat(val) / 1000) * vid.duration;
vid.pause();
vid.currentTime = t;
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(t);
}
function sceneToggleGrid() {
_sceneGridOpen = !_sceneGridOpen;
renderSidebarScenery();
}
function sceneLibToggle() {
_sceneLibOpen = !_sceneLibOpen;
if (_sceneLibOpen && !_sceneLib) _sceneLib = null; // force re-fetch on open
renderSidebarScenery();
}
// Use a library scenery image as slot 0 background (fetched to bytes).
async function sceneLibUseAsBackground(filename) {
try {
const url = IMAGE_FOLDER + encodeURIComponent(filename);
const b64 = await _fetchAsBase64(url);
_sceneRefs[0] = { kind: 'bytes', data: b64 };
_sceneVideo = null;
_sceneFrameBytes = null;
_sceneGridOpen = false;
renderSidebarScenery();
showToast('Background set from previous scenery', 'success');
} catch (e) {
showToast('Could not load image: ' + e, 'error');
}
}
// ---- scenery reference-slot helpers ----
// Thumbnail (URL or data-URL) for a slot ref.
function _sceneThumb(ref) {
if (!ref) return '';
if (ref.kind === 'bytes') return 'data:image/png;base64,' + ref.data;
const idx = lbNames.indexOf(ref.name);
const u = idx >= 0 ? lbUrls[idx] : (IMAGE_FOLDER + ref.name);
return isVideo(ref.name) ? posterFor(u) : u;
}
function _sceneUrlForName(name) {
const idx = lbNames.indexOf(name);
return idx >= 0 ? lbUrls[idx] : (IMAGE_FOLDER + name);
}
async function _fetchAsBase64(url) {
const r = await fetch(url);
const blob = await r.blob();
return await new Promise((res, rej) => {
const fr = new FileReader();
fr.onload = () => res(String(fr.result).split(',')[1]);
fr.onerror = rej;
fr.readAsDataURL(blob);
});
}
// Refresh both the scenery panel and the filmstrip badges.
function _sceneRefresh() { renderSidebarScenery(); updateStudio(); }
// Assign a filmstrip file to the first empty slot scanning right→left ([2,1,0]).
// Shift+Click an already-assigned file removes it (toggle).
function sceneAssignRef(name) {
const at = _sceneRefs.findIndex(r => r && r.kind === 'file' && r.name === name);
if (at >= 0) { _sceneRefs[at] = null; _sceneRefresh(); return; }
for (const i of [2, 1, 0]) {
if (!_sceneRefs[i]) {
_sceneRefs[i] = { kind: 'file', name };
if (i === 0) _sceneFrameBytes = null; // a file now backs slot 0
_sceneRefresh();
return;
}
}
showToast('All 3 reference slots are full — ✕ one first', 'info');
}
function sceneClearSlot(i) {
const ref = _sceneRefs[i];
_sceneRefs[i] = null;
if (i === 0 && ref && ref.kind === 'bytes') _sceneFrameBytes = null;
if (i === 1) _scenePersonInit = true; // don't auto-refill after explicit clear
_sceneRefresh();
}
// ---- drag & drop between slots / from filmstrip ----
function sceneDragSlotStart(ev, i) {
if (!_sceneRefs[i]) { ev.preventDefault(); return; }
_sceneDrag = { type: 'slot', idx: i };
ev.dataTransfer.effectAllowed = 'move';
}
function sceneFilmDragStart(ev, i) {
_sceneDrag = { type: 'file', name: lbNames[i] };
ev.dataTransfer.effectAllowed = 'copy';
}
function sceneDragOver(ev) { ev.preventDefault(); ev.currentTarget.classList.add('drop-hover'); }
function sceneDragLeave(ev) { ev.currentTarget.classList.remove('drop-hover'); }
function sceneDrop(ev, dst) {
ev.preventDefault();
ev.currentTarget.classList.remove('drop-hover');
const drag = _sceneDrag; _sceneDrag = null;
if (!drag) return;
if (drag.type === 'file') {
// Dropping a filmstrip image onto a slot — remove any duplicate first.
const dup = _sceneRefs.findIndex(r => r && r.kind === 'file' && r.name === drag.name);
if (dup >= 0 && dup !== dst) _sceneRefs[dup] = null;
if (dst === 0 && _sceneRefs[0] && _sceneRefs[0].kind === 'bytes') _sceneFrameBytes = null;
_sceneRefs[dst] = { kind: 'file', name: drag.name };
} else {
const src = drag.idx;
if (src === dst) return;
const a = _sceneRefs[src], b = _sceneRefs[dst];
// Captured/uploaded bytes can only live in slot 0 (the background).
if ((a && a.kind === 'bytes' && dst !== 0) || (b && b.kind === 'bytes' && src !== 0)) {
showToast('The captured background can only sit in the Image 1 slot', 'info');
return;
}
_sceneRefs[src] = b; _sceneRefs[dst] = a;
_sceneFrameBytes = (_sceneRefs[0] && _sceneRefs[0].kind === 'bytes') ? _sceneRefs[0].data : null;
}
_sceneRefresh();
}
async function sceneSelectVideo(v) {
_sceneVideo = v; _sceneFrameBytes = null;
if (_sceneRefs[0] && _sceneRefs[0].kind === 'bytes') _sceneRefs[0] = null;
_sceneGridOpen = false; // collapse the picker so the player is front-and-centre
renderSidebarScenery();
}
function sceneReleaseFrame() {
_sceneFrameBytes = null;
if (_sceneRefs[0] && _sceneRefs[0].kind === 'bytes') _sceneRefs[0] = null;
renderSidebarScenery(); // re-render restores the live video + slider + capture button
}
async function sceneExtractFrame() {
const vid = document.getElementById('sceneVideoEl');
if (vid) vid.pause();
const t = vid ? vid.currentTime : 0;
const captureBtn = document.getElementById('sceneCaptureBtn');
if (captureBtn) { captureBtn.disabled = true; captureBtn.textContent = 'Extracting…'; }
try {
const r = await fetch(`${API}/wireframe/frame`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_name: _sceneVideo, time: t }),
});
if (!r.ok) {
showToast('Frame extract failed: ' + (await r.text()), 'error');
if (captureBtn) { captureBtn.disabled = false; captureBtn.textContent = '📷 Capture this frame'; }
} else {
const d = await r.json();
_sceneFrameBytes = d.frame_b64;
_sceneRefs[0] = { kind: 'bytes', data: d.frame_b64 }; // background → slot 0
_sceneRefresh(); // show the frozen frame + update slot badge
}
} catch (e) {
showToast('Frame extract error: ' + e, 'error');
if (captureBtn) { captureBtn.disabled = false; captureBtn.textContent = '📷 Capture this frame'; }
}
}
function sceneHandleUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
_sceneFrameBytes = e.target.result.split(',')[1];
_sceneRefs[0] = { kind: 'bytes', data: _sceneFrameBytes }; // background → slot 0
_sceneVideo = null;
renderSidebarScenery();
};
reader.readAsDataURL(file);
}
async function submitGenerateScenery() {
// Map positional slots → backend contract.
const bg = _sceneRefs[0], person = _sceneRefs[1], extra = _sceneRefs[2];
if (!bg) { showToast('Set a background (Image 1) — capture, upload, or Shift+Click', 'error'); return; }
if (!person || person.kind !== 'file') { showToast('Set a person image (Image 2) from the filmstrip', 'error'); return; }
if (extra && extra.kind !== 'file') { showToast('Image 3 must be a gallery image', 'error'); return; }
const prompt = (document.getElementById('scenePromptInput')?.value || '').trim() || null;
const btn = document.getElementById('sceneGenBtn');
const statusEl = document.getElementById('sceneJobStatus');
const textEl = document.getElementById('sceneJobText');
if (btn) btn.disabled = true;
if (statusEl) statusEl.style.display = 'inline-flex';
if (textEl) textEl.textContent = 'Submitting…';
if (prompt) savePromptHistory(prompt);
// slot0 → scene_bytes: bytes directly, or fetch a file-as-background to bytes.
let sceneBytes;
try {
sceneBytes = (bg.kind === 'bytes') ? bg.data : 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 = {
model_filename: person.name, // image2 (Picture 2) — person
scene_bytes: sceneBytes, // image1 (Picture 1) — background
scene_video: null,
scene_time: 0,
extra_filename: (extra && extra.kind === 'file') ? extra.name : null, // image3 (Picture 3)
prompt,
seed: -1,
};
try {
const r = await fetch(`${API}/generate-scenery`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) {
if (textEl) textEl.textContent = 'Error: ' + await r.text();
if (btn) btn.disabled = false; return;
}
const { job_id } = await r.json();
showToast('Scenery generation started…', 'info');
if (textEl) textEl.textContent = 'Generating…';
const pollScene = () => {
_sceneJobPollTimer = setTimeout(async () => {
try {
const jr = await fetch(`${API}/batch/${job_id}`);
if (!jr.ok) { pollScene(); return; }
const job = await jr.json();
if (job.status === 'running') { if (textEl) textEl.textContent = 'Generating…'; pollScene(); return; }
if (job.status === 'done') {
showToast('Scenery generated!', 'success');
if (statusEl) statusEl.style.display = 'none';
if (btn) btn.disabled = false;
// Auto-follow so the open studio filmstrip jumps to the
// new image (matches the Generate tab); refreshNow() alone
// doesn't update an already-open studio's filmstrip.
_followLatestGid = lbCurrentGid;
refreshNow();
} else {
const errMsg = job.error || 'unknown error';
if (textEl) textEl.textContent = 'Error: ' + errMsg;
if (btn) btn.disabled = false;
showToast('Scenery failed: ' + errMsg, 'error');
}
} catch (e) { pollScene(); }
}, 2500);
};
pollScene();
} catch (e) {
if (textEl) textEl.textContent = 'API error: ' + e;
if (btn) btn.disabled = false;
}
}
// ---- segment (SAM2 / rembg) sidebar tab ----
function renderSidebarSegment() {
const panel = document.getElementById('sbPanelSegment');
if (!panel) return;
const savedScrollTop = panel.scrollTop;
const isChecked = document.getElementById('studioViewer')?.classList.contains('show-checker');
panel.innerHTML = `
<div style="font-size:11px;color:#666;margin-bottom:12px;line-height:1.6">
Remove background using SAM2 or rembg fallback. The original file is
kept intact — a sidecar <code>.nobg.png</code> is created. Restore BG
simply shows the original again without any file change.
</div>
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
<button class="sb-btn" id="sbSam2Btn" onclick="sam2RemoveBg()" title="SAM2 segmentation — preserves hair/glasses/accessories; falls back to rembg if unavailable">Remove BG (SAM2)</button>
<button class="sb-btn" id="sbRembgBtn" onclick="rembgRemoveBg()" title="rembg (U2Net) — fast background removal without SAM2">Remove BG (rembg)</button>
<button class="sb-btn" id="sbRestoreBgBtn" onclick="restoreBg()">Restore BG</button>
</div>
<div id="sbSegStatus" style="font-size:11px;color:#aaa;min-height:16px;margin-bottom:10px"></div>
<div class="sb-sep"></div>
<label style="display:flex;align-items:center;gap:8px;font-size:12px;cursor:pointer;margin-bottom:6px">
<input type="checkbox" id="sbCheckerboard" onchange="toggleCheckerboard(this.checked)" ${isChecked ? 'checked' : ''}>
<span>Checkerboard (show transparency)</span>
</label>
<div style="font-size:10px;color:#555;line-height:1.4">
SAM2 merges all non-border masks (preserves hair, glasses, sandals).<br>
rembg (U2Net) is faster and works without SAM2 checkpoint.<br>
Requires sam2_checkpoint in config.json for SAM2 path.
</div>`;
// Check SAM2 availability
fetch(`${API}/sam2/check`).then(r => r.json()).then(d => {
const btn = document.getElementById('sbSam2Btn');
if (btn) btn.title = d.sam2 ? 'SAM2 available' : 'SAM2 not available — will use rembg fallback';
}).catch(()=>{});
panel.scrollTop = savedScrollTop;
requestAnimationFrame(() => {
if (panel) panel.scrollTop = savedScrollTop;
});
}
async function sam2RemoveBg() {
const fname = lbNames[lbIdx];
if (!fname) return;
const btn = document.getElementById('sbSam2Btn');
const statusEl = document.getElementById('sbSegStatus');
if (btn) btn.disabled = true;
if (statusEl) statusEl.textContent = 'Removing background…';
try {
const r = await fetch(`${API}/remove-background-sam/${encodeURIComponent(fname)}`, { method: 'POST' });
if (!r.ok) {
if (statusEl) statusEl.textContent = 'Error: ' + await r.text();
} else {
const d = await r.json();
const label = d.used_sam2 ? 'Done (SAM2)' : 'Done (rembg)';
if (statusEl) statusEl.textContent = label;
showToast('Background removed', 'success');
refreshNow(true);
// Build URL same way as all gallery images: IMAGE_FOLDER + filename
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
_nobgSidecars.set(fname, nobgUrl);
// Preload before swap to avoid black flash
const preload = new Image();
preload.onload = () => {
const img = document.getElementById('lbImg');
if (img) img.src = nobgUrl;
const cb = document.getElementById('sbCheckerboard');
if (cb && !cb.checked) { cb.checked = true; toggleCheckerboard(true); }
};
preload.src = nobgUrl;
}
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
if (btn) btn.disabled = false;
}
function restoreBg() {
// Non-destructive: just revert the viewer to the original URL.
// The sidecar .nobg.png stays on disk for future use.
const fname = lbNames[lbIdx];
if (!fname) return;
_nobgSidecars.delete(fname);
const img = document.getElementById('lbImg');
if (img) img.src = lbUrls[lbIdx] + (lbUrls[lbIdx].includes('?') ? '&' : '?') + 't=' + Date.now();
const statusEl = document.getElementById('sbSegStatus');
if (statusEl) statusEl.textContent = 'Original restored';
// Turn off checkerboard
const cb = document.getElementById('sbCheckerboard');
if (cb && cb.checked) { cb.checked = false; toggleCheckerboard(false); }
showToast('Original image shown', 'success');
}
async function rembgRemoveBg() {
const fname = lbNames[lbIdx];
if (!fname) return;
const btn = document.getElementById('sbRembgBtn');
const statusEl = document.getElementById('sbSegStatus');
if (btn) btn.disabled = true;
if (statusEl) statusEl.textContent = 'Removing background (rembg)…';
try {
const r = await fetch(`${API}/remove-background/${encodeURIComponent(fname)}`, { method: 'POST' });
if (!r.ok) {
if (statusEl) statusEl.textContent = 'Error: ' + await r.text();
} else {
const d = await r.json();
if (statusEl) statusEl.textContent = 'Done (rembg)';
showToast('Background removed (rembg)', 'success');
refreshNow(true);
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
_nobgSidecars.set(fname, nobgUrl);
const preload = new Image();
preload.onload = () => {
const img = document.getElementById('lbImg');
if (img) img.src = nobgUrl;
const cb = document.getElementById('sbCheckerboard');
if (cb && !cb.checked) { cb.checked = true; toggleCheckerboard(true); }
};
preload.src = nobgUrl;
}
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
if (btn) btn.disabled = false;
}
function toggleCheckerboard(on, isRetry=false) {
const viewer = document.getElementById('studioViewer');
const cbLayer = document.getElementById('studioCheckerboard');
const img = document.getElementById('lbImg');
if (viewer) viewer.classList.toggle('show-checker', on);
if (cbLayer) {
if (on && img && img.style.display !== 'none') {
const rect = getImageDisplayRect(img);
if (rect) {
cbLayer.style.display = 'block';
cbLayer.style.left = rect.left + 'px';
cbLayer.style.top = rect.top + 'px';
cbLayer.style.width = rect.width + 'px';
cbLayer.style.height = rect.height + 'px';
} else {
cbLayer.style.display = 'none';
}
} else {
cbLayer.style.display = 'none';
}
}
const cb = document.getElementById('sbCheckerboard');
if (cb) cb.checked = on;
}
// ---- set preferred ----
async function lbSetPreferred() {
const fname = lbNames[lbIdx];
if (!fname) return;
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/set-preferred`, {method:'POST'});
if (!r.ok) return;
fileSortOrders[fname] = 0;
const url = lbUrls[lbIdx];
const otherUrls = lbUrls.filter((_, i) => i !== lbIdx);
const otherNames = lbNames.filter((_, i) => i !== lbIdx);
lbUrls = [url, ...otherUrls];
lbNames = [fname, ...otherNames];
lbIdx = 0;
updateStudio();
refreshNow(true);
// Fire-and-forget face extraction for face-book
fetch(`${API}/images/${encodeURIComponent(fname)}/extract-face`, { method: 'POST' })
.then(r => r.ok ? r.json() : null)
.then(d => {
if (d?.status === 'queued') {
showToast('Extracting face reference…', 'info');
loadFaceThumb(lbCurrentGid, 8); // poll until the crop lands
}
})
.catch(() => {});
} catch(e) { console.error(e); }
}
function showPasteGroupDialog(files) {
document.getElementById('pasteDialog')?.remove();
const gName = (lbCurrentGid && groupNames[lbCurrentGid]) || lbCurrentGid || 'current group';
const gidSafe = escHtml(lbCurrentGid || '');
const gNameSafe = escHtml(gName);
const d = document.createElement('div');
d.id = 'pasteDialog';
d.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:20px;z-index:600;min-width:280px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,0.7)';
d.innerHTML = `<div style="font-size:13px;color:#ccc;margin-bottom:14px">Add ${files.length} image(s)?</div>
<div style="display:flex;flex-direction:column;gap:8px">
<button class="btn primary" onclick="handleUploadToGroup(window._pasteFiles,'${gidSafe}');document.getElementById('pasteDialog')?.remove()">Add to &ldquo;${gNameSafe}&rdquo; — no poses</button>
<button class="btn" onclick="handleUpload(window._pasteFiles);document.getElementById('pasteDialog')?.remove()">New group (run poses)</button>
<button class="btn" onclick="document.getElementById('pasteDialog')?.remove()">Cancel</button>
</div>`;
window._pasteFiles = files;
document.body.appendChild(d);
setTimeout(() => document.getElementById('pasteDialog')?.remove(), 12000);
}
async function handleUploadToGroup(files, groupId) {
showToast(`Adding ${files.length} image(s) to group…`);
const added = [];
for (const file of files) {
const fd = new FormData();
fd.append('image', file);
fd.append('group_id', groupId);
fd.append('skip_poses', 'true');
try {
const r = await fetch(`${API}/upload`, { method: 'POST', body: fd });
if (r.ok) {
const d = await r.json().catch(() => ({}));
if (d.filename) added.push(d.filename);
} else {
showToast('Upload failed: ' + await r.text(), 'error');
}
} catch (e) { showToast('Upload error: ' + e, 'error'); }
}
// Optimistically show the new images immediately — the static images.json the
// gallery reads from is rebuilt on a background thread, so refreshNow() alone
// would race ahead of it and the images wouldn't appear until the next refresh.
if (added.length) {
const fresh = groupData.get(groupId);
added.forEach(fn => {
const url = IMAGE_FOLDER + fn + '?t=' + Date.now();
if (fresh && !fresh.names.includes(fn)) {
fresh.urls.push(url);
fresh.names.push(fn);
if (fresh.visibleIdx) fresh.visibleIdx.push(fresh.names.length - 1);
}
if (_isStudioOpen() && lbCurrentGid === groupId && !lbNames.includes(fn)) {
lbUrls.push(url);
lbNames.push(fn);
}
});
if (_isStudioOpen() && lbCurrentGid === groupId) updateStudio();
}
showToast('Added to group', 'success');
// Reconcile with the backend once the static data file has been regenerated.
setTimeout(refreshNow, 600);
}
</script>
</body>
</html>