diff --git a/backlog.md b/backlog.md
index db148d2..accee85 100644
--- a/backlog.md
+++ b/backlog.md
@@ -69,7 +69,10 @@ Scenery:
- samengestelde outpaint featurue lijkt niet goed te werken, but individual steps work perfect.
- regenerate a single orbit - pose
- when generating a known pose, we should know roughly the image ratio. Make a pose-table, when a pose is generated store the wireframe and meta data
-- we can rate an image if its OVER21, manually now in the UI. this is fine, however we want to backfill this TAG for evey image
+- we can rate an image if its OVER21, manually now in the UI. this is fine, however we want to backfill this TAG for evey image
+- backfill, people-count, 21+, IS_COMPLETE, RESTRAINT
+- the wireframe / videos / sub-clips worden niet direct weergegeven
+-
## refine
- when refresh page, we lose track of current jobs running.
diff --git a/ph_downloader.py b/ph_downloader.py
new file mode 100755
index 0000000..d7298da
--- /dev/null
+++ b/ph_downloader.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+"""
+Pornhub Video Downloader Side-Script
+Uses yt-dlp under the hood for highly robust and fast downloads.
+"""
+
+import sys
+import os
+import subprocess
+
+# Ensure yt-dlp and curl-cffi are installed
+try:
+ import yt_dlp
+ import curl_cffi
+except ImportError:
+ print("[-] yt-dlp or curl-cffi is not fully installed in this Python environment.")
+ print("[*] Attempting to install yt-dlp with curl-cffi automatically...")
+ try:
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp[default,curl-cffi]"])
+ import yt_dlp
+ import curl_cffi
+ print("[+] Successfully installed yt-dlp and curl-cffi!")
+ except Exception as e:
+ print(f"[!] Failed to install dependencies automatically: {e}")
+ print("[!] Please install manually with: pip install \"yt-dlp[default,curl-cffi]\"")
+ sys.exit(1)
+
+import argparse
+
+def download_video(url, output_dir=".", quality="best"):
+ print(f"[*] Initializing download for: {url}")
+ print(f"[*] Output directory: {os.path.abspath(output_dir)}")
+
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Format selector configuration
+ # 'best' is the safest format because it downloads pre-merged streams and doesn't strictly require ffmpeg.
+ # 'bestvideo+bestaudio/best' will download separate video/audio streams and merge them if ffmpeg is present.
+ format_selector = 'best'
+ if quality != 'best':
+ if quality.endswith('p'):
+ height = quality[:-1]
+ format_selector = f'bestvideo[height<={height}]+bestaudio/best[height<={height}]'
+ else:
+ format_selector = quality
+
+ ydl_opts = {
+ 'format': format_selector,
+ 'outtmpl': os.path.join(output_dir, '%(title)s [%(id)s].%(ext)s'),
+ 'noplaylist': True,
+ # Pornhub has age gates; standard user-agent and headers are handled by yt-dlp automatically,
+ # but let's add some basic robust options.
+ 'ignoreerrors': False,
+ 'logtostderr': False,
+ 'quiet': False,
+ 'no_warnings': False,
+ 'nocheckcertificate': True,
+ }
+
+ # Setup browser impersonation to bypass Cloudflare/TLS fingerprinting blocks (e.g. HTTP 403 Forbidden)
+ try:
+ from yt_dlp.networking.impersonate import ImpersonateTarget
+ ydl_opts['impersonate'] = ImpersonateTarget.from_str('chrome')
+ except (ImportError, AttributeError):
+ ydl_opts['impersonate'] = 'chrome'
+
+ try:
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
+ info = ydl.extract_info(url, download=True)
+ filename = ydl.prepare_filename(info)
+ print("\n[+] Download completed successfully!")
+ print(f"[+] File saved to: {filename}")
+ return True
+ except Exception as e:
+ print(f"\n[!] Error downloading video: {e}")
+ # Try a safe fallback to 'best' format if we tried a complex format query
+ if format_selector != 'best':
+ print("[*] Retrying with fallback quality ('best')...")
+ ydl_opts['format'] = 'best'
+ try:
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
+ info = ydl.extract_info(url, download=True)
+ filename = ydl.prepare_filename(info)
+ print("\n[+] Download completed successfully via fallback!")
+ print(f"[+] File saved to: {filename}")
+ return True
+ except Exception as fe:
+ print(f"[!] Fallback download also failed: {fe}")
+ return False
+
+def main():
+ parser = argparse.ArgumentParser(description="Pornhub / General Video Downloader")
+ parser.add_argument("url", nargs="?", help="URL of the video to download")
+ parser.add_argument("-o", "--output", default="/mnt/zim/tour-comfy/wireframe/", help="Output directory path (default: current directory)")
+ parser.add_argument("-q", "--quality", default="720p", help="Video quality (e.g. best, 1080p, 720p, 480p) (default: 720p)")
+
+ args = parser.parse_args()
+
+ url = args.url
+ if not url:
+ # Interactive mode
+ try:
+ url = input("Enter video URL (e.g., Pornhub link): ").strip()
+ except (KeyboardInterrupt, EOFError):
+ print("\n[-] Cancelled.")
+ sys.exit(0)
+
+ if not url:
+ print("[!] No URL provided. Exiting.")
+ sys.exit(1)
+
+ success = download_video(url, args.output, args.quality)
+ sys.exit(0 if success else 1)
+
+if __name__ == "__main__":
+ main()
diff --git a/tour-comfy/car.html b/tour-comfy/car.html
index b187c1c..2f793e4 100644
--- a/tour-comfy/car.html
+++ b/tour-comfy/car.html
@@ -2048,6 +2048,8 @@
Move to Hidden
Move to Source
Move to Archived
+ Relocate to Group...
+ Extract Solo
@@ -2077,6 +2079,7 @@
Scene
SAM
Orbit
+ Design
@@ -2114,6 +2117,37 @@
+
+
+ -
+ -
+
+
+ -
+ TRACKING
+
+
+
+
+
@@ -2130,7 +2164,7 @@
Crop
Duplicate
Crop…
- Expand…
+ Pad
⟲ 90°
⟳ 90°
Pose
@@ -2148,6 +2182,7 @@
+
@@ -2299,6 +2334,327 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
+ "20260629_120459_fs_bitch insemination_dup_20260626_003125_dup_Pasted image (9).png.mp4",
+ "20260624_154536_img_72.png",
+ "20260629_114451_sc_sc_image.nobg.png",
+ "20260629_114346_sc_sc_image.nobg.png",
+ "_turntable/cg_4e2b6449/views/view_023_345deg.png",
+ "_turntable/up_ad4000d7/views/view_023_345deg.png",
+ "_turntable/up_ad4000d7/views/view_022_330deg.png",
+ "_turntable/up_ad4000d7/views/view_021_315deg.png",
+ "_turntable/up_ad4000d7/views/view_020_300deg.png",
+ "_turntable/up_ad4000d7/views/view_019_285deg.png",
+ "_turntable/up_ad4000d7/views/view_018_270deg.png",
+ "_turntable/up_ad4000d7/views/view_017_255deg.png",
+ "_turntable/up_ad4000d7/views/view_016_240deg.png",
+ "_turntable/up_ad4000d7/views/view_015_225deg.png",
+ "_turntable/up_ad4000d7/views/view_014_210deg.png",
+ "_turntable/up_ad4000d7/views/view_013_195deg.png",
+ "_turntable/up_ad4000d7/views/view_012_180deg.png",
+ "_turntable/up_ad4000d7/views/view_011_165deg.png",
+ "_turntable/up_ad4000d7/views/view_010_150deg.png",
+ "_turntable/up_ad4000d7/views/view_009_135deg.png",
+ "_turntable/up_ad4000d7/views/view_008_120deg.png",
+ "_turntable/up_ad4000d7/views/view_007_105deg.png",
+ "_turntable/up_ad4000d7/views/view_006_090deg.png",
+ "_turntable/up_ad4000d7/views/view_005_075deg.png",
+ "_turntable/up_ad4000d7/views/view_004_060deg.png",
+ "_turntable/up_ad4000d7/views/view_003_045deg.png",
+ "_turntable/up_ad4000d7/views/view_002_030deg.png",
+ "_turntable/up_ad4000d7/views/view_001_015deg.png",
+ "_turntable/up_ad4000d7/views/view_000_000deg.png",
+ "_turntable/cg_e6ba3260/views/view_023_345deg.png",
+ "_turntable/cg_e6ba3260/views/view_022_330deg.png",
+ "_turntable/cg_e6ba3260/views/view_021_315deg.png",
+ "_turntable/cg_e6ba3260/views/view_020_300deg.png",
+ "_turntable/cg_e6ba3260/views/view_019_285deg.png",
+ "_turntable/cg_e6ba3260/views/view_018_270deg.png",
+ "_turntable/cg_e6ba3260/views/view_017_255deg.png",
+ "_turntable/cg_e6ba3260/views/view_016_240deg.png",
+ "_turntable/cg_e6ba3260/views/view_015_225deg.png",
+ "_turntable/cg_e6ba3260/views/view_014_210deg.png",
+ "_turntable/cg_e6ba3260/views/view_013_195deg.png",
+ "_turntable/cg_e6ba3260/views/view_012_180deg.png",
+ "_turntable/cg_e6ba3260/views/view_011_165deg.png",
+ "_turntable/cg_e6ba3260/views/view_010_150deg.png",
+ "_turntable/cg_e6ba3260/views/view_009_135deg.png",
+ "_turntable/cg_e6ba3260/views/view_008_120deg.png",
+ "_turntable/cg_e6ba3260/views/view_007_105deg.png",
+ "_turntable/cg_e6ba3260/views/view_006_090deg.png",
+ "_turntable/cg_e6ba3260/views/view_005_075deg.png",
+ "_turntable/cg_e6ba3260/views/view_004_060deg.png",
+ "_turntable/cg_e6ba3260/views/view_003_045deg.png",
+ "_turntable/cg_e6ba3260/views/view_002_030deg.png",
+ "_turntable/cg_e6ba3260/views/view_001_015deg.png",
+ "_turntable/cg_e6ba3260/views/view_000_000deg.png",
+ "_turntable/cg_a9282b3b/views/view_009_135deg.png",
+ "_turntable/cg_a9282b3b/views/view_008_120deg.png",
+ "_turntable/cg_a9282b3b/views/view_007_105deg.png",
+ "_turntable/cg_a9282b3b/views/view_006_090deg.png",
+ "_turntable/cg_a9282b3b/views/view_005_075deg.png",
+ "_turntable/cg_a9282b3b/views/view_004_060deg.png",
+ "_turntable/cg_a9282b3b/views/view_003_045deg.png",
+ "_turntable/cg_a9282b3b/views/view_002_030deg.png",
+ "_turntable/cg_a9282b3b/views/view_001_015deg.png",
+ "_turntable/cg_a9282b3b/views/view_000_000deg.png",
+ "20260629_063504_12_20260629_063237_image.png",
+ "20260629_063452_11_20260629_063237_image.png",
+ "20260629_063441_10_20260629_063237_image.png",
+ "20260629_063430_9_20260629_063237_image.png",
+ "20260629_063418_8_20260629_063237_image.png",
+ "20260629_063407_7_20260629_063237_image.png",
+ "20260629_063356_6_20260629_063237_image.png",
+ "20260629_063345_5_20260629_063237_image.png",
+ "20260629_063333_4_20260629_063237_image.png",
+ "20260629_063322_3_20260629_063237_image.png",
+ "20260629_063311_2_20260629_063237_image.png",
+ "20260629_063300_1_20260629_063237_image.png",
+ "20260629_063248_0_20260629_063237_image.png",
+ "20260629_063237_image.png",
+ "20260629_063200_12_20260629_062932_image.png",
+ "20260629_063149_11_20260629_062932_image.png",
+ "20260629_063138_10_20260629_062932_image.png",
+ "20260629_063127_9_20260629_062932_image.png",
+ "20260629_063116_8_20260629_062932_image.png",
+ "20260629_063105_7_20260629_062932_image.png",
+ "20260629_063054_6_20260629_062932_image.png",
+ "20260629_063043_5_20260629_062932_image.png",
+ "20260629_063032_4_20260629_062932_image.png",
+ "20260629_063022_3_20260629_062932_image.png",
+ "20260629_063011_2_20260629_062932_image.png",
+ "20260629_063000_1_20260629_062932_image.png",
+ "20260629_062949_0_20260629_062932_image.png",
+ "20260629_062932_image.png",
+ "20260629_060926_12_20260629_060652_image.png",
+ "20260629_060915_11_20260629_060652_image.png",
+ "20260629_060904_10_20260629_060652_image.png",
+ "20260629_060853_9_20260629_060652_image.png",
+ "20260629_060842_8_20260629_060652_image.png",
+ "20260629_060831_7_20260629_060652_image.png",
+ "20260629_060820_6_20260629_060652_image.png",
+ "20260629_060809_5_20260629_060652_image.png",
+ "20260629_060747_3_20260629_060652_image.png",
+ "20260629_060736_2_20260629_060652_image.png",
+ "20260629_060725_1_20260629_060652_image.png",
+ "20260629_060714_0_20260629_060652_image.png",
+ "20260629_060652_image.png",
+ "cg_fb1043d8_face.png",
+ "20260629_055242_image.png",
+ "_turntable/cg_2bbb563a/views/view_022_330deg.png",
+ "_turntable/cg_2bbb563a/views/view_003_045deg.png",
+ "_turntable/cg_2bbb563a/views/20260629_054801_dup_view_003_045deg.png",
+ "_turntable/cg_2bbb563a/views/view_023_345deg.png",
+ "_turntable/cg_2bbb563a/views/view_021_315deg.png",
+ "_turntable/cg_2bbb563a/views/view_020_300deg.png",
+ "_turntable/cg_2bbb563a/views/view_019_285deg.png",
+ "_turntable/cg_2bbb563a/views/view_018_270deg.png",
+ "_turntable/cg_2bbb563a/views/view_017_255deg.png",
+ "_turntable/cg_2bbb563a/views/view_016_240deg.png",
+ "_turntable/cg_2bbb563a/views/view_015_225deg.png",
+ "_turntable/cg_2bbb563a/views/view_014_210deg.png",
+ "_turntable/cg_2bbb563a/views/view_013_195deg.png",
+ "_turntable/cg_2bbb563a/views/view_012_180deg.png",
+ "_turntable/cg_2bbb563a/views/view_011_165deg.png",
+ "_turntable/cg_2bbb563a/views/view_010_150deg.png",
+ "_turntable/cg_2bbb563a/views/view_009_135deg.png",
+ "_turntable/cg_2bbb563a/views/view_008_120deg.png",
+ "_turntable/cg_2bbb563a/views/view_007_105deg.png",
+ "_turntable/cg_2bbb563a/views/view_006_090deg.png",
+ "_turntable/cg_2bbb563a/views/view_005_075deg.png",
+ "_turntable/cg_2bbb563a/views/view_004_060deg.png",
+ "_turntable/cg_2bbb563a/views/view_002_030deg.png",
+ "_turntable/cg_2bbb563a/views/view_001_015deg.png",
+ "_turntable/cg_2bbb563a/views/view_000_000deg.png",
+ "cg_2bbb563a_face.png",
+ "20260629_044126_sc_image.png",
+ "20260629_043959_sc_image.png",
+ "20260628_154609_image.nobg.png",
+ "20260629_043853_sc_image.png",
+ "20260629_042417_sc_image.png",
+ "20260629_041152_sc_image.nobg.png",
+ "20260629_041152_sc_image.png",
+ "20260629_040857_sc_image.png",
+ "20260629_040648_sc_image.png",
+ "20260629_040418_dup_20260621_032709_image.nobg.nobg.png",
+ "20260629_040418_dup_20260621_032709_image.nobg.png",
+ "20260629_040025_sc_image.nobg.png",
+ "_turntable/image.png/views/20260629_035933_sc_view_000_000deg.png",
+ "20260629_034754_sc_0_20260622_101128_image.png",
+ "20260629_034712_sc_0_20260622_101128_image.png",
+ "20260629_034537_sc_0_20260622_101128_image.png",
+ "20260629_034527_sc_0_20260622_101128_image.png",
+ "20260629_034144_sc_0_20260622_101128_image.png",
+ "20260629_034023_sc_0_20260622_101128_image.png",
+ "20260629_033851_sc_0_20260622_101128_image.png",
+ "20260629_033631_sc_2_20260622_101128_image.nobg.png",
+ "20260629_032345_mr_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_032136_mr_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_031800_mr_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_031259_image.png",
+ "20260629_030904_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_030742_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_030705_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_030537_sc_image.nobg.nobg.png",
+ "20260629_025630_sc_image.nobg.nobg.png",
+ "20260629_024315_image.nobg.nobg.png",
+ "20260629_025420_sc_image.nobg.png",
+ "20260629_024911_sc_image.nobg.png",
+ "20260629_024824_sc_image.nobg.png",
+ "20260629_024707_image.nobg.png",
+ "20260629_024707_image.png",
+ "20260629_024511_sc_image.nobg.png",
+ "20260629_024413_sc_image.nobg.png",
+ "20260629_024305_sc_dup_20260624_055327_image.nobg.nobg.nobg.png",
+ "20260629_024315_image.nobg.png",
+ "20260629_024305_sc_dup_20260624_055327_image.nobg.nobg.png",
+ "20260629_024246_mr_sc_dup_20260624_055327_image.nobg.nobg.png",
+ "20260629_024037_sc_dup_20260624_055327_image.nobg.nobg.png",
+ "20260629_021748_dup_20260624_055327_image.nobg.nobg.png",
+ "20260624_055049_image.nobg.png",
+ "20260629_015442_image.nobg.png",
+ "20260629_015127_image.nobg.png",
+ "20260629_012800_mr_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_012559_mr_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_012137_sc_image.nobg.png",
+ "20260629_012025_sc_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_011925_sc_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_011741_sc_dup_20260624_055327_image.nobg.png",
+ "20260628_192939_dup_20260624_055327_image.nobg.nobg.png",
+ "20260629_004439_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_004407_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_004301_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_004236_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_003942_sc_dup_20260624_055327_image.nobg.png",
+ "20260629_003839_sc_image.nobg.png",
+ "20260629_003636_sc_image.nobg.png",
+ "20260629_003507_sc_image.nobg.png",
+ "20260629_003223_sc_image.png",
+ "20260629_003132_sc_image.png",
+ "20260629_003014_sc_image.nobg.png",
+ "20260629_002909_sc_image.nobg.png",
+ "20260629_002329_3_20260628_205049_image.png",
+ "20260629_002318_3_20260628_205049_image.png",
+ "20260629_002307_3_20260628_205049_image.png",
+ "20260629_002128_image.png",
+ "20260629_002117_image.png",
+ "20260629_002030_3_20260628_205049_image.png",
+ "20260629_002019_3_20260628_205049_image.png",
+ "20260629_001558_image.nobg.png",
+ "20260629_001547_image.nobg.png",
+ "20260629_001536_image.nobg.png",
+ "20260629_001525_image.nobg.png",
+ "20260629_001514_image.nobg.png",
+ "20260629_001502_image.nobg.png",
+ "20260629_001451_image.nobg.png",
+ "20260629_001429_image.nobg.png",
+ "20260629_001418_image.nobg.png",
+ "20260629_001408_image.nobg.png",
+ "20260629_001357_image.nobg.png",
+ "20260629_001225_image.nobg.png",
+ "image.png_face.png",
+ "20260629_000834_image.nobg.png",
+ "20260629_000906_image.png",
+ "20260629_000855_image.png",
+ "20260629_000844_image.png",
+ "20260629_000834_image.png",
+ "20260629_000532_image.png",
+ "20260629_000232_image.png",
+ "20260629_000101_image.png",
+ "20260629_000022_image.png",
+ "20260628_235923_3_20260628_205049_image.png",
+ "20260628_235601_3_20260628_205049_image.png",
+ "20260628_235157_image.png",
+ "20260628_234315_image.png",
+ "_turntable/image.png/views/view_000_000deg.png",
+ "20260628_222828_0_20260628_222816_image.png",
+ "_turntable/image.png/views/view_023_345deg.png",
+ "_turntable/image.png/views/view_022_330deg.png",
+ "_turntable/image.png/views/view_021_315deg.png",
+ "_turntable/image.png/views/view_020_300deg.png",
+ "_turntable/image.png/views/view_019_285deg.png",
+ "_turntable/image.png/views/view_018_270deg.png",
+ "_turntable/image.png/views/view_017_255deg.png",
+ "_turntable/image.png/views/view_016_240deg.png",
+ "_turntable/image.png/views/view_015_225deg.png",
+ "_turntable/image.png/views/view_014_210deg.png",
+ "_turntable/image.png/views/view_013_195deg.png",
+ "_turntable/image.png/views/view_012_180deg.png",
+ "_turntable/image.png/views/view_011_165deg.png",
+ "_turntable/image.png/views/view_010_150deg.png",
+ "_turntable/image.png/views/view_009_135deg.png",
+ "_turntable/image.png/views/view_008_120deg.png",
+ "_turntable/image.png/views/view_007_105deg.png",
+ "_turntable/image.png/views/view_006_090deg.png",
+ "_turntable/image.png/views/view_005_075deg.png",
+ "_turntable/image.png/views/view_004_060deg.png",
+ "_turntable/image.png/views/view_003_045deg.png",
+ "_turntable/image.png/views/view_002_030deg.png",
+ "_turntable/image.png/views/view_001_015deg.png",
+ "20260628_205437_12_20260628_205049_image.png",
+ "20260628_205421_11_20260628_205049_image.png",
+ "20260628_205404_10_20260628_205049_image.png",
+ "20260628_205348_9_20260628_205049_image.png",
+ "20260628_205336_8_20260628_205049_image.png",
+ "20260628_205325_7_20260628_205049_image.png",
+ "20260628_205302_5_20260628_205049_image.png",
+ "20260628_205240_3_20260628_205049_image.png",
+ "20260628_205229_2_20260628_205049_image.png",
+ "20260628_205217_1_20260628_205049_image.png",
+ "20260628_205206_0_20260628_205049_image.png",
+ "20260628_205049_image.png",
+ "20260628_204427_image.png",
+ "20260628_192939_dup_20260624_055327_image.nobg.png",
+ "cg_18040099_face.png",
+ "20260628_191913_dup_20260624_055327_image.nobg.png",
+ "20260628_191837_dup_20260624_055327_image.nobg.png",
+ "20260628_191628_dup_20260624_055327_image.nobg.png",
+ "20260628_191504_dup_20260624_055327_image.nobg.png",
+ "20260628_191427_dup_20260624_055327_image.nobg.png",
+ "20260628_191411_dup_20260624_055327_image.nobg.png",
+ "20260628_191311_dup_20260624_055327_image.nobg.png",
+ "20260628_191244_dup_20260624_055327_image.nobg.png",
+ "20260628_191156_dup_20260624_055327_image.nobg.png",
+ "20260628_191124_dup_20260624_055327_image.nobg.png",
+ "20260628_190719_dup_20260624_055327_image.nobg.png",
+ "20260628_190648_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "20260628_190535_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "20260628_190448_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "_turntable/up_f475c600/views/20260628_190319_view_023_345deg.png",
+ "20260628_184957_image.png",
+ "20260628_184858_dup_20260624_055327_image.nobg.png",
+ "20260628_175708_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "20260628_175639_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "20260628_175556_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "20260628_175521_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
+ "cg_f0049873_face.png",
+ "20260628_173904_dup_20260624_055327_image.nobg.png",
+ "_turntable/up_f475c600/views/20260628_173436_view_023_345deg.png",
+ "_turntable/up_f475c600/views/20260628_173114_view_023_345deg.png",
+ "20260628_173104_up_f475c600_face.png",
+ "_turntable/up_f475c600/views/view_023_345deg.png",
+ "20260628_172359_image.png",
+ "cg_d032c1ef_face.png",
+ "_turntable/cg_d032c1ef/views/view_023_345deg.png",
+ "_turntable/cg_d032c1ef/views/view_000_000deg.png",
+ "_turntable/cg_d032c1ef/views/view_009_135deg.png",
+ "_turntable/cg_d032c1ef/views/view_001_015deg.png",
+ "_turntable/cg_d032c1ef/views/view_002_030deg.png",
+ "20260618_054230_image.png_face.png",
+ "cg_f1e85987_face.png",
+ "up_f475c600_face.png",
+ "20260618_174516_image.png_face.png",
+ "up_76ef7e4f_face.png",
+ "cg_e79533a4_face.png",
+ "_turntable/cg_e79533a4/views/view_023_345deg.png",
+ "_turntable/cg_e79533a4/views/view_000_000deg.png",
+ "_turntable/cg_e79533a4/views/view_012_180deg.png",
+ "_turntable/cg_e79533a4/views/view_011_165deg.png",
+ "cg_61b863a8_face.png",
+ "up_a4d16da7_face.png",
+ "up_455c085f_face.png",
+ "up_b64786e7_face.png",
+ "20260618_133729_image.png_face.png",
+ "img_77.png_face.png",
"20260628_165000_sc_image.png",
"20260628_164850_sc_sc_image.png",
"20260628_154819_sc_image.png",
@@ -2339,8 +2695,8 @@
"20260626_231655_sc_image.nobg.png",
"20260618_181600_image.png_face.png",
"_turntable/20260618_181600_image.png/views/view_000_000deg.png",
- "_turntable/cg_74544975/views/20260628_140758_view_000_000deg.nobg.png",
"_turntable/cg_74544975/views/20260628_142102_dup_20260628_140758_view_000_000deg.nobg.png",
+ "_turntable/cg_74544975/views/20260628_140758_view_000_000deg.nobg.png",
"_turntable/cg_74544975/views/20260628_140758_view_000_000deg.png",
"up_3a05a2de_face.png",
"up_aa1add4d_face.png",
@@ -2974,7 +3330,6 @@
"_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",
@@ -3277,7 +3632,6 @@
"_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",
@@ -3968,7 +4322,6 @@
"_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",
@@ -4253,7 +4606,6 @@
"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",
@@ -4287,7 +4639,6 @@
"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_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.png",
"20260627_030344_sc_image.png",
@@ -4389,7 +4740,6 @@
"_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",
@@ -4403,16 +4753,12 @@
"_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",
@@ -4509,7 +4855,6 @@
"_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",
@@ -4607,7 +4952,6 @@
"_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",
@@ -4865,7 +5209,6 @@
"_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",
@@ -4933,7 +5276,6 @@
"_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",
@@ -4944,8 +5286,6 @@
"_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",
@@ -4956,7 +5296,6 @@
"_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",
@@ -5016,8 +5355,8 @@
"20260626_025708_2_20260625_205722_image.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_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",
@@ -5113,11 +5452,11 @@
"20260625_025335_dup_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",
- "20260625_023536_dup_20260624_165109_0_20260622_091731_image.png",
"20260624_165109_0_20260622_091731_image.png",
+ "20260625_023536_dup_20260624_165109_0_20260622_091731_image.png",
"up_ab858b0a_face.png",
- "20260625_022250_sc_image.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",
@@ -5312,9 +5651,9 @@
"20260624_183010_0_20260624_182958_image.png",
"20260624_182958_image.png",
"20260624_174900_image.png",
+ "20260624_173115_image.png",
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
"20260624_174943_dup_20260624_173115_image.png",
- "20260624_173115_image.png",
"20260624_174757_image.png",
"20260624_174442_image.png",
"20260624_172923_image.nobg.png",
@@ -5337,8 +5676,8 @@
"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_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_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",
@@ -5371,7 +5710,6 @@
"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",
@@ -5610,7 +5948,6 @@
"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",
@@ -6137,8 +6474,8 @@
"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_020521_dup_20260622_020223_image.png",
"20260622_013636_image.nobg.png",
"20260622_015031_image.png",
"20260622_013636_image.png",
@@ -6162,8 +6499,8 @@
"20260620_152017_image.png",
"20260618_013452_out4.png",
"20260620_025959_out4.png",
- "20260622_021658_dup_20260621_212650_image.png",
"20260621_212650_image.png",
+ "20260622_021658_dup_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",
@@ -6432,7 +6769,6 @@
"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",
@@ -7010,8 +7346,8 @@
"t159zr-1.png",
"kbk99v.png",
"kk563t.png",
- "Pasted image (9).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",
@@ -7082,6 +7418,10 @@
let fileArchived = {}; // filename → boolean
let filePoseDescription = {}; // filename → human readable string
let filePoseSkeleton = {}; // filename → JSON string (COCO-17 keypoints)
+ let filePeopleCount = {}; // filename → integer or null
+ let fileAnatomicalCompleteness = {}; // filename → boolean or null
+ let fileFacialDirection = {}; // filename → string or null
+ let fileObjects = {}; // filename → array of objects
let fileTags = {}; // filename → array of strings
let fileIsSource = {}; // filename → boolean
let _archiveViewActive = false; // when true, show archived items only
@@ -7186,6 +7526,62 @@
return group.gid;
}
+ function getGalleryColumnCount() {
+ const gallery = document.getElementById('gallery');
+ if (!gallery) return 1;
+ const cards = gallery.querySelectorAll('.image-card');
+ if (cards.length < 2) return 1;
+ let count = 0;
+ const firstTop = cards[0].offsetTop;
+ for (let i = 0; i < cards.length; i++) {
+ if (cards[i].offsetTop === firstTop) count++;
+ else break;
+ }
+ return count || 1;
+ }
+
+ function updateGalleryHighlight() {
+ const cards = document.querySelectorAll('.image-card');
+ let found = null;
+ cards.forEach(card => {
+ const gid = card.dataset.group;
+ const isMatch = gid === _lastSelectedGid;
+ card.classList.toggle('highlight-prev', isMatch);
+ if (isMatch) found = card;
+ });
+ if (found && !document.getElementById('studio').classList.contains('open')) {
+ found.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+ }
+ }
+
+ function handleGalleryNav(e) {
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
+ if (!currentGroups || currentGroups.length === 0) return;
+ let idx = currentGroups.findIndex(g => g.gid === _lastSelectedGid);
+ if (idx === -1) idx = 0;
+ const cols = getGalleryColumnCount();
+ if (e.key === 'ArrowLeft') idx--;
+ else if (e.key === 'ArrowRight') idx++;
+ else if (e.key === 'ArrowUp') idx -= cols;
+ else if (e.key === 'ArrowDown') idx += cols;
+ if (idx < 0) idx = 0;
+ if (idx >= currentGroups.length) idx = currentGroups.length - 1;
+ const newGid = currentGroups[idx].gid;
+ if (newGid !== _lastSelectedGid) {
+ _lastSelectedGid = newGid;
+ localStorage.setItem('lastSelectedGid', _lastSelectedGid);
+ updateGalleryHighlight();
+ }
+ e.preventDefault();
+ } else if (e.key === 'Enter') {
+ const card = [...document.querySelectorAll('.image-card')].find(c => c.dataset.group === _lastSelectedGid);
+ if (card) {
+ handleCardClick(card);
+ e.preventDefault();
+ }
+ }
+ }
+
function clearCycleTimers() {
cycleTimers.forEach(id => clearInterval(id));
cycleTimers.clear();
@@ -7259,6 +7655,10 @@
const isHidden = !!fileHidden[fname];
const isSource = !!fileIsSource[fname];
+ if (fileGroups[fname] !== lbCurrentGid) {
+ return;
+ }
+
if (tab === 'hidden' && isHidden && !isArchived) {
matched.push(fname);
} else if (tab === 'source' && isSource && !isArchived) {
@@ -7362,6 +7762,94 @@
}
}
+ async function bulkRelocateGroup() {
+ if (_selectedFilenames.size === 0) return;
+ const targetGid = prompt("Enter target Group ID to move the selected images to (or leave blank to generate a new unique group):");
+ if (targetGid === null) return; // User canceled
+
+ const list = Array.from(_selectedFilenames);
+ try {
+ const r = await fetch(`${API}/groups/merge`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ filenames: list, group_id: targetGid.trim() || null })
+ });
+ if (r.ok) {
+ const res = await r.json();
+ const finalGid = res.group_id;
+ showToast(`Moved ${list.length} images to group: ${finalGid}`, 'success');
+
+ // Update local customGroups and fileGroups mapping immediately
+ list.forEach(fname => {
+ customGroups[fname] = finalGid;
+ fileGroups[fname] = finalGid;
+ });
+
+ toggleMultiSelectMode();
+
+ if (finalGid === lbCurrentGid) {
+ switchFilmstripTab(_activeFilmstripTab);
+ } else {
+ const remaining = lbNames.filter(n => !list.includes(n));
+ if (remaining.length === 0) {
+ closeLightbox();
+ } else {
+ lbIdx = lbNames.indexOf(remaining[0]);
+ if (lbIdx === -1) lbIdx = 0;
+ switchFilmstripTab(_activeFilmstripTab);
+ }
+ }
+ await loadImages();
+ } else {
+ const err = await r.text();
+ showToast(`Failed to relocate group: ${err}`, 'error');
+ }
+ } catch (e) {
+ showToast(`Network error: ${e}`, 'error');
+ }
+ }
+
+ async function bulkExtractSelected() {
+ if (_selectedFilenames.size === 0) return;
+ if (!confirm(`Are you sure you want to extract ${_selectedFilenames.size} selected image(s) into their own solo groups?`)) return;
+
+ const list = Array.from(_selectedFilenames);
+ let countSuccess = 0;
+ try {
+ await Promise.all(list.map(async (filename) => {
+ 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}`;
+ fileGroups[filename] = filename;
+ countSuccess++;
+ }
+ } catch (e) {
+ console.error("Error extracting " + filename, e);
+ }
+ }));
+
+ showToast(`Successfully extracted ${countSuccess} of ${list.length} images into solo groups`, 'success');
+ toggleMultiSelectMode();
+
+ const remaining = lbNames.filter(n => !list.includes(n));
+ if (remaining.length === 0) {
+ closeLightbox();
+ } else {
+ lbIdx = lbNames.indexOf(remaining[0]);
+ if (lbIdx === -1) lbIdx = 0;
+ switchFilmstripTab(_activeFilmstripTab);
+ }
+ await loadImages();
+ } catch (e) {
+ showToast(`Error during bulk extraction: ${e}`, 'error');
+ }
+ }
+
function filmstripThumbDragStart(event, fname) {
event.dataTransfer.setData('text/plain', fname);
event.dataTransfer.effectAllowed = 'move';
@@ -7652,13 +8140,54 @@
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
if (uniformEl) uniformEl.checked = false;
if (isSb) _sbPadUniform = false;
+ } else if (presetVal.includes(':')) {
+ // Aspect ratio presets (e.g. "1:1", "4:3")
+ const img = document.getElementById('lbImg');
+ if (!img || !img.naturalWidth) {
+ showToast('No active image to calculate aspect ratio', 'info');
+ return;
+ }
+ const parts = presetVal.split(':');
+ const targetAspect = parseFloat(parts[0]) / parseFloat(parts[1]);
+ const w = img.naturalWidth;
+ const h = img.naturalHeight;
+ const currentAspect = w / h;
+
+ // Reset all first
+ sides.forEach(side => {
+ const el = document.getElementById(prefix + side);
+ if (el) el.value = '0.00%';
+ });
+
+ if (currentAspect > targetAspect) {
+ // Too wide -> add height (Top/Bottom)
+ const targetH = w / targetAspect;
+ const totalPad = Math.round(targetH - h);
+ const half = Math.round(totalPad / 2);
+ const topEl = document.getElementById(prefix + 'Top');
+ const bottomEl = document.getElementById(prefix + 'Bottom');
+ if (topEl) topEl.value = half + 'px';
+ if (bottomEl) bottomEl.value = (totalPad - half) + 'px';
+ } else if (currentAspect < targetAspect) {
+ // Too tall -> add width (Left/Right)
+ const targetW = h * targetAspect;
+ const totalPad = Math.round(targetW - w);
+ const half = Math.round(totalPad / 2);
+ const leftEl = document.getElementById(prefix + 'Left');
+ const rightEl = document.getElementById(prefix + 'Right');
+ if (leftEl) leftEl.value = half + 'px';
+ if (rightEl) rightEl.value = (totalPad - half) + 'px';
+ }
+ const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
+ if (uniformEl) uniformEl.checked = false;
+ if (isSb) _sbPadUniform = false;
} else {
- // If it ends with %
+ // Fixed increment presets
let formatted = presetVal;
if (presetVal.endsWith('%')) {
formatted = parseFloat(presetVal).toFixed(2) + '%';
} else if (presetVal.endsWith('px')) {
- formatted = parseFloat(presetVal).toString();
+ formatted = parseInt(presetVal) + 'px';
}
sides.forEach(side => {
@@ -7833,6 +8362,7 @@
const v = document.getElementById(id);
if (v) { v.pause(); v.removeAttribute('src'); v.load(); }
});
+ updateGalleryHighlight();
}
function toggleSlideshow() {
@@ -8219,11 +8749,65 @@
if (desc || kpts) {
poseInfoWrap.style.display = 'block';
poseDescEl.textContent = desc || 'Pose detected';
- _renderPoseToCanvas(poseCanvas, kpts);
+ _renderPoseToCanvas(poseCanvas, kpts, fname);
+
+ const hudGaze = document.getElementById('lbPoseHudGaze');
+ const hudCompl = document.getElementById('lbPoseHudCompl');
+ const hudCount = document.getElementById('lbPoseHudCount');
+ if (hudGaze) {
+ const g = fileFacialDirection[fname];
+ hudGaze.textContent = g ? `GAZE: ${g.toUpperCase()}` : 'GAZE: -';
+ }
+ if (hudCompl) {
+ const c = fileAnatomicalCompleteness[fname];
+ hudCompl.textContent = c !== null ? (c ? 'BODY: FULL' : 'BODY: PARTIAL') : 'BODY: -';
+ }
+ if (hudCount) {
+ const n = filePeopleCount[fname];
+ hudCount.textContent = n !== null ? `SUBJECTS: ${n}` : 'SUBJECTS: -';
+ }
} else {
poseInfoWrap.style.display = 'none';
}
}
+
+ const deepMetaWrap = document.getElementById('lbDeepMetaWrap');
+ const deepPeopleCount = document.getElementById('lbDeepPeopleCount');
+ const deepCompleteness = document.getElementById('lbDeepCompleteness');
+ const deepFacialDirection = document.getElementById('lbDeepFacialDirection');
+ const deepObjects = document.getElementById('lbDeepObjects');
+ if (deepMetaWrap) {
+ const count = filePeopleCount[fname];
+ const compl = fileAnatomicalCompleteness[fname];
+ const faceDir = fileFacialDirection[fname];
+ const objs = fileObjects[fname] || [];
+ const hasDeepMeta = count !== null || compl !== null || faceDir !== null || objs.length > 0;
+ if (hasDeepMeta) {
+ deepMetaWrap.style.display = 'block';
+ if (deepPeopleCount) {
+ deepPeopleCount.textContent = count !== null ? `${count} person(s)` : '-';
+ }
+ if (deepCompleteness) {
+ deepCompleteness.textContent = compl !== null ? (compl ? 'Complete / Full body' : 'Partial / Close-up') : '-';
+ }
+ if (deepFacialDirection) {
+ deepFacialDirection.textContent = faceDir || '-';
+ }
+ if (deepObjects) {
+ if (objs.length > 0) {
+ deepObjects.innerHTML = objs.map(o => {
+ const tag = typeof o === 'string' ? o : (o.tag || '');
+ const score = typeof o === 'object' && o.score ? ` (${Math.round(o.score * 100)}%)` : '';
+ return `${escHtml(tag)}${score} `;
+ }).join('');
+ } else {
+ deepObjects.innerHTML = 'None detected ';
+ }
+ }
+ } else {
+ deepMetaWrap.style.display = 'none';
+ }
+ }
}
// Alias for old callsites
@@ -8713,7 +9297,7 @@
}
const POSE_SKELETON = [[5,7],[7,9],[6,8],[8,10],[11,13],[13,15],[12,14],[14,16],[5,6],[11,12],[5,11],[6,12],[0,1],[0,2],[1,3],[2,4],[0,5],[0,6]];
- function _renderPoseToCanvas(canvas, kptsJson) {
+ function _renderPoseToCanvas(canvas, kptsJson, fname) {
if (!canvas || !kptsJson) return;
let kpts;
try { kpts = typeof kptsJson === 'string' ? JSON.parse(kptsJson) : kptsJson; } catch(e) { return; }
@@ -8727,12 +9311,32 @@
const minX = Math.min(...xs), maxX = Math.max(...xs);
const minY = Math.min(...ys), maxY = Math.max(...ys);
const w = maxX - minX, h = maxY - minY;
- const margin = 10;
+ const margin = 15;
const scale = Math.min((canvas.width - margin*2) / (w || 1), (canvas.height - margin*2) / (h || 1));
const offX = margin - minX * scale + (canvas.width - margin*2 - w * scale) / 2;
const offY = margin - minY * scale + (canvas.height - margin*2 - h * scale) / 2;
const cx = x => offX + x * scale, cy = y => offY + y * scale;
+ // Draw a subtle framing/bounding box for anatomical completeness
+ if (fname) {
+ const isCompl = fileAnatomicalCompleteness[fname];
+ if (isCompl !== undefined && isCompl !== null) {
+ ctx.save();
+ if (isCompl) {
+ ctx.strokeStyle = 'rgba(16, 185, 129, 0.25)'; // semi-transparent green
+ ctx.lineWidth = 1;
+ ctx.setLineDash([4, 4]);
+ ctx.strokeRect(4, 4, canvas.width - 8, canvas.height - 8);
+ } else {
+ ctx.strokeStyle = 'rgba(245, 158, 11, 0.25)'; // semi-transparent amber
+ ctx.lineWidth = 1;
+ ctx.setLineDash([2, 4]);
+ ctx.strokeRect(8, 8, canvas.width - 16, canvas.height - 16);
+ }
+ ctx.restore();
+ }
+ }
+
ctx.lineWidth = 2; ctx.strokeStyle = '#3b82f6';
POSE_SKELETON.forEach(([a, b]) => {
const pa = kpts[a], pb = kpts[b];
@@ -8744,6 +9348,43 @@
ctx.beginPath(); ctx.arc(cx(p[0]), cy(p[1]), 2, 0, Math.PI * 2);
ctx.fillStyle = '#fff'; ctx.fill();
});
+
+ // Draw sci-fi gaze and target if nose is visible
+ if (kpts[0] && kpts[0][2] >= POSE_MIN_SCORE) {
+ const nx = cx(kpts[0][0]);
+ const ny = cy(kpts[0][1]);
+
+ // Gaze direction
+ let gazeDir = fname ? fileFacialDirection[fname] : null;
+ if (gazeDir) {
+ gazeDir = gazeDir.toLowerCase();
+ ctx.save();
+ ctx.beginPath();
+ ctx.strokeStyle = '#ef4444'; // Red gaze sight vector
+ ctx.lineWidth = 1.5;
+ ctx.setLineDash([2, 2]);
+ ctx.moveTo(nx, ny);
+ if (gazeDir.includes('left')) {
+ ctx.lineTo(nx - 20, ny);
+ } else if (gazeDir.includes('right')) {
+ ctx.lineTo(nx + 20, ny);
+ } else if (gazeDir.includes('up')) {
+ ctx.lineTo(nx, ny - 20);
+ } else if (gazeDir.includes('down')) {
+ ctx.lineTo(nx, ny + 20);
+ } else { // 'front'
+ // concentric circles for front gaze
+ ctx.setLineDash([]);
+ ctx.strokeStyle = 'rgba(239, 68, 68, 0.4)';
+ ctx.arc(nx, ny, 8, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.arc(nx, ny, 4, 0, Math.PI * 2);
+ }
+ ctx.stroke();
+ ctx.restore();
+ }
+ }
}
function _poseHitTest(mx, my) {
@@ -8852,6 +9493,7 @@
if (btn) { btn.disabled = false; btn.textContent = 'Generate ▸'; }
if (job.status === 'done') {
showToast('Pose generation done!', 'success');
+ _followLatestFilename = job.output;
_followLatestGid = lbCurrentGid;
refreshNow(true);
} else {
@@ -9288,13 +9930,17 @@
+ `+10% `
+ `+20% `
+ `+50% `
- + `+100px `
+ + `+100px `
+ + `1:1 `
+ + `4:3 `
+ + `3:4 `
+ + `16:9 `
+ `Reset `
+ `Custom: `
- + numInput('padTop','↑')
- + numInput('padRight','→')
- + numInput('padBottom','↓')
- + numInput('padLeft','←')
+ + numInput('padTop','Top:')
+ + numInput('padRight','Right:')
+ + numInput('padBottom','Bottom:')
+ + numInput('padLeft','Left:')
+ ``
+ ` Uniform `
+ `
@@ -9496,6 +10142,7 @@
if (!r.ok) { showToast('Video failed: ' + await r.text(), 'error'); return; }
const d = await r.json();
showToast(`Video created: ${d.output}`, 'success');
+ _followLatestFilename = d.output;
_followLatestGid = lbCurrentGid;
refreshNow(true);
} catch (e) { showToast('Video error: ' + e, 'error'); }
@@ -10243,6 +10890,10 @@
fileArchived[img.filename] = !!img.archived;
filePoseDescription[img.filename] = img.pose_description || null;
filePoseSkeleton[img.filename] = img.pose_skeleton || null;
+ filePeopleCount[img.filename] = img.people_count ?? null;
+ fileAnatomicalCompleteness[img.filename] = img.anatomical_completeness ?? null;
+ fileFacialDirection[img.filename] = img.facial_direction || null;
+ fileObjects[img.filename] = img.objects || [];
fileTags[img.filename] = img.tags || [];
fileIsSource[img.filename] = !!img.is_source;
fileHasBg[img.filename] = img.has_background !== false;
@@ -10283,7 +10934,7 @@
// Fallback: live API (handles first startup before static file is written)
if (!files) {
try {
- const url = `${API}/images?archived=true`;
+ const url = `${API}/images?archived=true${bypassStatic ? '&bypass_static=true' : ''}`;
const r = await fetch(url, { signal: AbortSignal.timeout(12000) });
if (r.ok) {
const data = await r.json();
@@ -10372,7 +11023,7 @@
lbUrls = urls;
lbNames = names;
updateStudio();
- } else if (_followLatestGid === gid && _isStudioOpen() && lbCurrentGid === gid && names.length > 0) {
+ } else if (!_followLatestFilename && _followLatestGid === gid && _isStudioOpen() && lbCurrentGid === gid && names.length > 0) {
_followLatestGid = null;
lbIdx = names.length - 1;
lbUrls = urls;
@@ -10505,6 +11156,7 @@
}
if (statusDot) statusDot.classList.remove('updating');
+ updateGalleryHighlight();
loadImages._inFlight = false;
}
@@ -10903,11 +11555,13 @@
dl.innerHTML = hist.map(p => ` `).join('');
}
- function savePromptHistory(prompt) {
- let hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
+ function savePromptHistory(prompt, key = 'batchPromptHistory') {
+ let hist = JSON.parse(localStorage.getItem(key) || '[]');
hist = [prompt, ...hist.filter(p => p !== prompt)].slice(0, 20);
- localStorage.setItem('batchPromptHistory', JSON.stringify(hist));
- loadPromptHistory();
+ localStorage.setItem(key, JSON.stringify(hist));
+ if (key === 'batchPromptHistory') {
+ loadPromptHistory();
+ }
}
// --- tagging ---
@@ -11072,8 +11726,12 @@
toggleSelectMode(); return;
}
}
- if (!document.getElementById('studio').classList.contains('open')) return;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
+ if (document.getElementById('variantPicker').classList.contains('open')) return;
+ if (!document.getElementById('studio').classList.contains('open')) {
+ handleGalleryNav(e);
+ return;
+ }
// Throttle filmstrip navigation — ignore if last nav was < 80 ms ago.
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
const now = Date.now();
@@ -11317,6 +11975,7 @@
if (tab === 'scenery') renderSidebarScenery();
if (tab === 'segment') renderSidebarSegment();
if (tab === 'orbit') renderSidebarOrbit();
+ if (tab === 'designer') renderSidebarDesigner();
}
function toggleImageZoom() {
@@ -11515,7 +12174,8 @@
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 `${side[0]}
+ const label = side === 'Top' ? 'T:' : side === 'Right' ? 'R:' : side === 'Bottom' ? 'B:' : 'L:';
+ return `${label}
+10%
+20%
+50%
+ 1:1
+ 4:3
+ 3:4
+ 16:9
Reset
Outpaint
@@ -11565,7 +12229,8 @@
function showPromptSuggest(el, boxId = 'sbPromptSuggest') {
const box = document.getElementById(boxId);
if (!box) return;
- const hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
+ const histKey = boxId === 'scenePromptSuggest' ? 'scenePromptHistory' : 'batchPromptHistory';
+ const hist = JSON.parse(localStorage.getItem(histKey) || '[]');
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; }
@@ -11844,11 +12509,12 @@
btn.disabled = true;
btn.innerHTML = ' Refining…';
+ const filename = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
try {
const r = await fetch(`${API}/refine-prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ prompt })
+ body: JSON.stringify({ prompt, filename })
});
if (r.ok) {
const { refined } = await r.json();
@@ -11901,11 +12567,12 @@
btn.disabled = true;
btn.innerHTML = ' Refining…';
+ const filename = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
try {
const r = await fetch(`${API}/refine-prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ prompt })
+ body: JSON.stringify({ prompt, filename })
});
if (r.ok) {
const { refined } = await r.json();
@@ -12335,6 +13002,7 @@
if (textEl) textEl.textContent = `Done! ${job.output || ''}`;
if (btn) btn.textContent = 'Done ✓';
showToast(`Faceswap complete: ${job.output || ''}`, 'success');
+ _followLatestFilename = job.output;
_followLatestGid = lbCurrentGid;
refreshNow(true);
} else {
@@ -12449,6 +13117,70 @@
// ---- scenery sidebar tab ----
let _sceneVideo = null, _sceneDuration = 0, _sceneFrameBytes = null, _sceneJobPollTimer = null;
+ let _sceneLastKey = null;
+
+ function getActiveSceneKey() {
+ const bg = _sceneRefs[0];
+ if (!bg) return null;
+ if (bg.kind === 'wireframe') return 'wf:' + bg.name;
+ if (bg.kind === 'file') return 'file:' + bg.name;
+ if (bg.kind === 'bytes') {
+ if (bg.filename) return 'lib:' + bg.filename;
+ return 'bytes:' + bg.data.length + '_' + bg.data.substring(0, 32);
+ }
+ return null;
+ }
+
+ function getScenePrompt(key) {
+ if (!key) return null;
+ const prompts = JSON.parse(localStorage.getItem('sceneSavedPrompts') || '{}');
+ return prompts[key] || null;
+ }
+
+ function saveScenePrompt(prompt) {
+ const key = getActiveSceneKey();
+ if (!key || !prompt) return;
+ const prompts = JSON.parse(localStorage.getItem('sceneSavedPrompts') || '{}');
+ prompts[key] = prompt;
+ localStorage.setItem('sceneSavedPrompts', JSON.stringify(prompts));
+ }
+
+ function saveScenePromptManual() {
+ const el = document.getElementById('scenePromptInput');
+ if (!el) return;
+ const prompt = el.value.trim();
+ if (!prompt) {
+ showToast('Please type a prompt first', 'info');
+ return;
+ }
+ saveScenePrompt(prompt);
+ showToast('Prompt saved for this background', 'success');
+ renderSidebarScenery();
+ }
+
+ function clearSavedScenePrompt() {
+ const key = getActiveSceneKey();
+ if (!key) return;
+ if (!confirm('Clear the saved prompt for this background?')) return;
+ const prompts = JSON.parse(localStorage.getItem('sceneSavedPrompts') || '{}');
+ delete prompts[key];
+ localStorage.setItem('sceneSavedPrompts', JSON.stringify(prompts));
+ showToast('Saved prompt removed', 'success');
+ renderSidebarScenery();
+ }
+
+ function useSavedScenePrompt() {
+ const key = getActiveSceneKey();
+ const savedPrompt = getScenePrompt(key);
+ if (!savedPrompt) return;
+ const el = document.getElementById('scenePromptInput');
+ if (el) {
+ el.value = savedPrompt;
+ autoGrowPrompt(el);
+ renderSidebarScenery();
+ showToast('Saved scene prompt loaded', 'success');
+ }
+ }
let _sceneGridOpen = false; // whether the video picker grid is expanded
let _sceneLibOpen = false; // whether "previous scenery" library panel is expanded
@@ -12466,7 +13198,16 @@
if (!panel) return;
const savedScrollTop = panel.scrollTop;
// Preserve a typed prompt across the frequent re-renders (slot changes).
- const prevScenePrompt = document.getElementById('scenePromptInput')?.value || '';
+ let prevScenePrompt = document.getElementById('scenePromptInput')?.value || '';
+
+ const activeSceneKey = getActiveSceneKey();
+ if (activeSceneKey !== _sceneLastKey) {
+ _sceneLastKey = activeSceneKey;
+ const savedPrompt = getScenePrompt(activeSceneKey);
+ if (savedPrompt !== null) {
+ prevScenePrompt = savedPrompt;
+ }
+ }
// Auto-fill the Person slot (image2) once per group with the current image.
const personName = _fsModelFilename || lbNames[lbIdx] || '';
if (!_scenePersonInit && !_sceneRefs[1] && personName) {
@@ -12655,7 +13396,34 @@
onkeydown="if(event.key==='Escape')hidePromptSuggest('scenePromptSuggest')"
style="resize:none;max-height:300px;overflow-y:auto;line-height:1.4;font-family:inherit">
-
+ `;
+
+ if (activeSceneKey) {
+ const savedPrompt = getScenePrompt(activeSceneKey);
+ if (savedPrompt) {
+ html += `
+
+
+ Saved Prompt for this Background:
+ Delete
+
+
${escHtml(savedPrompt)}
+
+ Reuse Prompt
+ Update with Current
+
+
`;
+ } else {
+ html += `
+
+ 💾 Save prompt to background
+
`;
+ }
+ }
+
+ html += `
Generating…
@@ -12764,7 +13532,7 @@
try {
const url = IMAGE_FOLDER + encodeURIComponent(filename);
const b64 = await _fetchAsBase64(url);
- _sceneRefs[0] = { kind: 'bytes', data: b64 };
+ _sceneRefs[0] = { kind: 'bytes', data: b64, filename: filename };
_sceneVideo = null;
_sceneFrameBytes = null;
_sceneGridOpen = false;
@@ -13025,7 +13793,10 @@
if (btn) btn.disabled = true;
if (statusEl) statusEl.style.display = 'inline-flex';
if (textEl) textEl.textContent = 'Submitting…';
- if (prompt) savePromptHistory(prompt);
+ if (prompt) {
+ savePromptHistory(prompt, 'scenePromptHistory');
+ saveScenePrompt(prompt);
+ }
// slot0 → scene_bytes: bytes directly, or fetch a file-as-background to bytes.
let sceneBytes = null;
let sceneImage = null;
@@ -13378,6 +14149,181 @@
// Reconcile with the backend once the static data file has been regenerated.
setTimeout(refreshNow, 600);
}
+
+ // ---- Prompt Designer sidebar tab ----
+
+ let _designerContext = "";
+ let _designerNum = 3;
+ let _designerUseActiveImg = true;
+ let _designerResults = null; // hold generated poses {"Name": "body"}
+
+ function renderSidebarDesigner() {
+ const panel = document.getElementById('sbPanelDesigner');
+ if (!panel) return;
+ const savedScrollTop = panel.scrollTop;
+
+ let html = `Prompt Designer
+
+ Generate multiple custom adult photographic pose prompts using the uncensored Mistral-24B model based on specific context guidelines or active image features.
+
`;
+
+ // Number of poses
+ html += `Number of Poses
+ `;
+
+ // Specific Context inputs
+ html += `Context / Guidelines
+ `;
+
+ // Checkbox for active image context
+ const activeFn = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
+ if (activeFn) {
+ const checkedAttr = _designerUseActiveImg ? ' checked' : '';
+ html += `
+
+ Use active image's context (pose, gaze, objects)
+ `;
+ } else {
+ html += `No active image selected to extract context.
`;
+ }
+
+ // Generate Button
+ html += `
+ Generate Poses
+ `;
+
+ // Results Container
+ html += ``;
+ if (_designerResults && Object.keys(_designerResults).length > 0) {
+ html += `
Generated Custom Poses
`;
+ Object.entries(_designerResults).forEach(([name, body]) => {
+ const nameEsc = escHtml(name);
+ const bodyEsc = escHtml(body);
+ const divId = 'designer_pose_' + name.replace(/\s+/g, '_');
+ html += `
+
+
${nameEsc}
+
+
+ Copy to Custom
+ Save to Library
+ Use & Switch
+
+
`;
+ });
+ } else if (_designerResults !== null) {
+ html += `
No valid poses were returned by the AI. Try adjusting guidelines.
`;
+ }
+ html += `
`;
+
+ panel.innerHTML = html;
+ panel.scrollTop = savedScrollTop;
+ }
+
+ async function submitDesignerGenerate() {
+ const btn = document.getElementById('designerGenerateBtn');
+ if (!btn) return;
+ const oldHtml = btn.innerHTML;
+ btn.disabled = true;
+ btn.innerHTML = ' Designing Poses…';
+
+ const activeFn = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
+ const payload = {
+ n: _designerNum,
+ context: _designerContext,
+ filename: _designerUseActiveImg ? activeFn : null
+ };
+
+ try {
+ const r = await fetch(`${API}/designer/generate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ if (r.ok) {
+ const data = await r.json();
+ _designerResults = data.poses;
+ showToast(`Successfully designed ${Object.keys(data.poses).length} custom poses`, 'success');
+ } else {
+ showToast('Designer failed: ' + await r.text(), 'error');
+ }
+ } catch (e) {
+ showToast('Designer error: ' + e, 'error');
+ } finally {
+ btn.disabled = false;
+ btn.innerHTML = oldHtml;
+ renderSidebarDesigner();
+ }
+ }
+
+ function designerLoadToCustomPrompt(name, btn) {
+ const box = btn.closest('.sb-pose-edit');
+ if (!box) return;
+ const textarea = box.querySelector('.sb-pose-text');
+ if (!textarea) return;
+ const promptVal = textarea.value.trim();
+
+ const el = document.getElementById('sbGenPromptInput');
+ if (el) {
+ el.value = promptVal;
+ autoGrowPrompt(el);
+ updateSbGenBtn();
+ showToast(`Pose "${name}" copied to custom prompt`, 'success');
+ } else {
+ showToast('Generate tab not ready or input not found.', 'error');
+ }
+ }
+
+ async function designerSaveToLibrary(name, btn) {
+ const box = btn.closest('.sb-pose-edit');
+ if (!box) return;
+ const textarea = box.querySelector('.sb-pose-text');
+ if (!textarea) return;
+ const promptVal = textarea.value.trim();
+
+ const oldHtml = btn.innerHTML;
+ btn.disabled = true;
+ btn.innerHTML = 'Saving…';
+
+ try {
+ const r = await fetch(`${API}/poses`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: name, text: promptVal, beta: false })
+ });
+ if (r.ok) {
+ const data = await r.json();
+ // Merge/Reload active poses list in memory
+ if (typeof loadPoses === 'function') {
+ await loadPoses();
+ } else if (typeof availablePoses === 'object') {
+ availablePoses[name] = { text: promptVal, beta: false };
+ }
+ showToast(`Pose "${name}" added to the permanent library`, 'success');
+ btn.innerHTML = 'Saved ✓';
+ btn.classList.add('primary');
+ } else {
+ showToast('Save failed: ' + await r.text(), 'error');
+ btn.disabled = false;
+ btn.innerHTML = oldHtml;
+ }
+ } catch (e) {
+ showToast('Save error: ' + e, 'error');
+ btn.disabled = false;
+ btn.innerHTML = oldHtml;
+ }
+ }
+
+ function designerLoadAndSwitch(name, btn) {
+ designerLoadToCustomPrompt(name, btn);
+ switchSidebarTab('generate');
+ }