Summary
• Implemented global offline mode for all HuggingFace-dependent modules to eliminate unauthenticated request warnings and prevent external connection attempts during startup and runtime. Changes • Global Offline Configuration: Added HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 to the environment variables at the top of edit_api.py, embeddings.py, watcher.py, and pose_llm/pose_llm_api.py. • Explicit Local Files Only: Updated all huggingface_hub.hf_hub_download calls in edit_api.py to include local_files_only=True, ensuring they never attempt to reach the Hub if models are already cached. • LLM Service Optimization: Updated AutoTokenizer and AutoModelForCausalLM calls in pose_llm/pose_llm_api.py to use local_files_only=True. • Consistency: Ensured that shared modules like embeddings.py which uses open_clip are also locked to offline mode to prevent background downloads.
This commit is contained in:
@@ -2299,7 +2299,55 @@
|
||||
|
||||
// --- HYDRATION_START ---
|
||||
const PRELOADED_IMAGES = [
|
||||
"20260628_133544_pad_20260621_032709_image.png",
|
||||
"20260628_165000_sc_image.png",
|
||||
"20260628_164850_sc_sc_image.png",
|
||||
"20260628_154819_sc_image.png",
|
||||
"20260628_154609_image.png",
|
||||
"20260628_154508_sc_image.png",
|
||||
"20260628_154429_dup_20260621_032709_image.png",
|
||||
"20260628_154339_image.png",
|
||||
"20260628_153602_sc_dup_20260621_032709_image.nobg.png",
|
||||
"20260628_153452_sc_image.png",
|
||||
"20260628_153333_sc_image.png",
|
||||
"20260628_151457_sc_image.png",
|
||||
"20260628_151037_sc_image.png",
|
||||
"up_6d311abe_face.png",
|
||||
"20260628_150512_dup_20260621_032709_image.nobg.png",
|
||||
"20260628_145644_dup_20260621_032709_image.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145317_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145305_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145254_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145243_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145232_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145221_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145209_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_145127_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||
"cg_2afca8fc_face.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_144836_view_001_015deg.nobg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_144836_view_001_015deg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_144825_view_001_015deg.png",
|
||||
"_turntable/cg_2afca8fc/views/20260628_144814_view_001_015deg.png",
|
||||
"cg_75340e8e_face.png",
|
||||
"cg_08203e88_face.png",
|
||||
"cg_74544975_face.png",
|
||||
"kk563t.png_face.png",
|
||||
"20260628_143350_kk563t.nobg.png",
|
||||
"20260628_143619_dup_20260628_143350_kk563t.nobg.png",
|
||||
"20260628_143350_kk563t.png",
|
||||
"20260628_143206_kk563t.png",
|
||||
"pass-1.png_face.png",
|
||||
"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.png",
|
||||
"up_3a05a2de_face.png",
|
||||
"up_aa1add4d_face.png",
|
||||
"20260628_134529_dup_20260621_032709_image.nobg.png",
|
||||
"_turntable/cg_7ec17537/views/20260628_135858_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||
"cg_7ec17537_face.png",
|
||||
"20260628_134529_dup_20260621_032709_image.png",
|
||||
"_turntable/up_7b6eaabb/views/view_023_345deg.png",
|
||||
"_turntable/up_7b6eaabb/views/view_022_330deg.png",
|
||||
"_turntable/up_7b6eaabb/views/view_021_315deg.png",
|
||||
@@ -2443,16 +2491,6 @@
|
||||
"_turntable/pass-1.png/views/view_008_120deg.png",
|
||||
"_turntable/pass-1.png/views/view_007_105deg.png",
|
||||
"_turntable/pass-1.png/views/view_006_090deg.png",
|
||||
"20260628_115611_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_115349_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_114542_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_114232_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_114212_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_113939_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_113737_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_113405_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_113320_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_113242_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_113152_sc_8_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"20260628_111908_sc_best_01_person_0.874_2026-06-27_12-18-49.png",
|
||||
"_turntable/pass-1.png/views/view_005_075deg.png",
|
||||
@@ -2477,20 +2515,15 @@
|
||||
"_turntable/cg_9bb8f3bb/views/view_010_150deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_009_135deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_008_120deg.png",
|
||||
"20260628_105942_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_007_105deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_006_090deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_005_075deg.png",
|
||||
"20260628_105539_sc_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_004_060deg.png",
|
||||
"20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_003_045deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_002_030deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_001_015deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_000_000deg.png",
|
||||
"_turntable/pa01.png/views/view_023_345deg.png",
|
||||
"20260628_105307_dup_20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"_turntable/pa01.png/views/view_022_330deg.png",
|
||||
"_turntable/pa01.png/views/view_021_315deg.png",
|
||||
"20260627_212812_5_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
@@ -2563,14 +2596,6 @@
|
||||
"_turntable/up_2687be75/views/view_002_030deg.png",
|
||||
"_turntable/up_2687be75/views/view_001_015deg.png",
|
||||
"_turntable/up_2687be75/views/view_000_000deg.png",
|
||||
"20260628_061520_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_061440_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_061355_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_061322_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_061249_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_061152_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_061006_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_060550_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
||||
"20260628_060113_sc_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"20260628_055858_sc_best_01_person_0.874_2026-06-27_12-18-49.png",
|
||||
"_turntable/cg_7ec17537/views/20260628_055541_sc_dup_20260628_033600_dup_view_000_000deg.png",
|
||||
@@ -2647,11 +2672,6 @@
|
||||
"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",
|
||||
@@ -2659,51 +2679,19 @@
|
||||
"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_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",
|
||||
@@ -2738,12 +2726,6 @@
|
||||
"_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",
|
||||
@@ -3259,7 +3241,6 @@
|
||||
"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",
|
||||
@@ -3275,7 +3256,6 @@
|
||||
"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",
|
||||
@@ -3286,11 +3266,6 @@
|
||||
"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",
|
||||
@@ -3345,27 +3320,13 @@
|
||||
"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",
|
||||
@@ -3382,12 +3343,7 @@
|
||||
"_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",
|
||||
@@ -3482,7 +3438,6 @@
|
||||
"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",
|
||||
@@ -3749,9 +3704,9 @@
|
||||
"_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/20260628_033831_dup_20260628_033600_dup_view_000_000deg.png",
|
||||
"_turntable/cg_7ec17537/views/20260628_033600_dup_view_000_000deg.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",
|
||||
@@ -4287,8 +4242,6 @@
|
||||
"_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",
|
||||
@@ -4296,16 +4249,6 @@
|
||||
"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",
|
||||
@@ -4317,8 +4260,6 @@
|
||||
"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",
|
||||
@@ -4348,7 +4289,6 @@
|
||||
"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",
|
||||
@@ -4402,10 +4342,6 @@
|
||||
"_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_234334_dup_20260626_231655_sc_image.png",
|
||||
"20260626_231655_sc_image.png",
|
||||
"20260626_231349_sc_image.png",
|
||||
@@ -4688,7 +4624,6 @@
|
||||
"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",
|
||||
@@ -5056,25 +4991,10 @@
|
||||
"_turntable/b1.png/views/view_002_030deg.png",
|
||||
"_turntable/b1.png/views/view_001_015deg.png",
|
||||
"_turntable/b1.png/views/view_000_000deg.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",
|
||||
"20260628_032038_dup_20260618_053932_7_20260618_053802_image.nobg.png",
|
||||
"20260626_034830_jb.nobg.png",
|
||||
"20260626_034830_jb.png",
|
||||
"20260626_034708_jb.png",
|
||||
@@ -5094,11 +5014,6 @@
|
||||
"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",
|
||||
@@ -5112,7 +5027,6 @@
|
||||
"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",
|
||||
@@ -5210,8 +5124,8 @@
|
||||
"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.png",
|
||||
"20260625_015711_sc_image.nobg.png",
|
||||
"20260625_015900_sc_image.png",
|
||||
"20260625_015413_sc_image.png",
|
||||
@@ -5422,8 +5336,6 @@
|
||||
"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",
|
||||
@@ -9308,12 +9220,38 @@
|
||||
cancelManualCrop();
|
||||
if (asCopy && d.new_filename) {
|
||||
showToast('Cropped to copy', '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];
|
||||
fileHidden[newFn] = false;
|
||||
fileIsSource[newFn] = false;
|
||||
fileArchived[newFn] = false;
|
||||
|
||||
// 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);
|
||||
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) {
|
||||
const gIdx = group.names.indexOf(fname);
|
||||
if (gIdx !== -1) {
|
||||
group.urls.splice(gIdx + 1, 0, lbUrls[lbIdx]);
|
||||
group.names.splice(gIdx + 1, 0, lbNames[lbIdx]);
|
||||
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
updateStudio();
|
||||
refreshNow();
|
||||
_followLatestFilename = newFn;
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow(true);
|
||||
} else {
|
||||
showToast('Cropped', 'success');
|
||||
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
|
||||
@@ -9414,6 +9352,9 @@
|
||||
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||
fileContentType[newFn] = 'image';
|
||||
fileSourceRefs[newFn] = [fname];
|
||||
fileHidden[newFn] = false;
|
||||
fileIsSource[newFn] = false;
|
||||
fileArchived[newFn] = false;
|
||||
|
||||
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
||||
lbNames.splice(lbIdx + 1, 0, newFn);
|
||||
@@ -9421,9 +9362,12 @@
|
||||
|
||||
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]]);
|
||||
const gIdx = group.names.indexOf(fname);
|
||||
if (gIdx !== -1) {
|
||||
group.urls.splice(gIdx + 1, 0, lbUrls[lbIdx]);
|
||||
group.names.splice(gIdx + 1, 0, lbNames[lbIdx]);
|
||||
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
updateStudio();
|
||||
@@ -9464,20 +9408,24 @@
|
||||
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||
fileContentType[newFn] = fileContentType[fname] || 'image';
|
||||
fileSourceRefs[newFn] = [fname];
|
||||
fileHidden[newFn] = false;
|
||||
fileIsSource[newFn] = false;
|
||||
fileArchived[newFn] = false;
|
||||
|
||||
// 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;
|
||||
// Append the duplicate to the end of the filmstrip
|
||||
lbUrls.push(IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
||||
lbNames.push(newFn);
|
||||
lbIdx = lbUrls.length - 1;
|
||||
|
||||
const group = groupData.get(lbCurrentGid);
|
||||
if (group) {
|
||||
group.urls.splice(lbIdx, 0, lbUrls[lbIdx]);
|
||||
group.names.splice(lbIdx, 0, lbNames[lbIdx]);
|
||||
group.urls.push(lbUrls[lbIdx]);
|
||||
group.names.push(lbNames[lbIdx]);
|
||||
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||
}
|
||||
|
||||
updateStudio();
|
||||
_followLatestFilename = newFn;
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow(true);
|
||||
} else {
|
||||
@@ -10507,21 +10455,50 @@
|
||||
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).
|
||||
|
||||
// Optimistic merge: retain any locally added names/urls that are not yet in the freshly loaded server list
|
||||
const mergedNames = [...fresh.names];
|
||||
const mergedUrls = [...fresh.urls];
|
||||
|
||||
for (let i = 0; i < lbNames.length; i++) {
|
||||
const localName = lbNames[i];
|
||||
if (!mergedNames.includes(localName)) {
|
||||
if (i === 0) {
|
||||
mergedNames.unshift(localName);
|
||||
mergedUrls.unshift(lbUrls[i]);
|
||||
} else {
|
||||
const prevLocal = lbNames[i - 1];
|
||||
const freshIdx = mergedNames.indexOf(prevLocal);
|
||||
if (freshIdx !== -1) {
|
||||
mergedNames.splice(freshIdx + 1, 0, localName);
|
||||
mergedUrls.splice(freshIdx + 1, 0, lbUrls[i]);
|
||||
} else {
|
||||
mergedNames.push(localName);
|
||||
mergedUrls.push(lbUrls[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update groupData as well so that other components have correct metadata
|
||||
fresh.names = mergedNames;
|
||||
fresh.urls = mergedUrls;
|
||||
fresh.visibleIdx = fresh.names.map((_, idx) => idx).filter(idx => !fileHidden[fresh.names[idx]]);
|
||||
|
||||
lbUrls = mergedUrls;
|
||||
lbNames = mergedNames;
|
||||
|
||||
const newIdx = lbNames.indexOf(curName);
|
||||
if (newIdx >= 0) {
|
||||
lbIdx = newIdx;
|
||||
} else if (lbIdx >= fresh.names.length) {
|
||||
lbIdx = fresh.names.length - 1;
|
||||
} else if (lbIdx >= lbNames.length) {
|
||||
lbIdx = lbNames.length - 1;
|
||||
}
|
||||
// Re-map multi-ref selections to new positions by filename
|
||||
_sbRefIndices = oldRefNames
|
||||
.map(n => fresh.names.indexOf(n))
|
||||
.map(n => lbNames.indexOf(n))
|
||||
.filter(i => i >= 0);
|
||||
updateStudio();
|
||||
}
|
||||
@@ -11266,13 +11243,32 @@
|
||||
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;
|
||||
const fresh = groupData.get(gid);
|
||||
if (fresh) {
|
||||
fresh.names.forEach(n => {
|
||||
if (n !== filename) {
|
||||
fileSortOrders[n] = (fileSortOrders[n] || 0) + 1;
|
||||
}
|
||||
});
|
||||
const paired = fresh.names.map((name, i) => ({ name, url: fresh.urls[i] }));
|
||||
paired.sort((a, b) => {
|
||||
const oa = fileSortOrders[a.name] ?? 9999;
|
||||
const ob = fileSortOrders[b.name] ?? 9999;
|
||||
if (oa !== ob) return oa - ob;
|
||||
return a.name.length - b.name.length;
|
||||
});
|
||||
fresh.names = paired.map(p => p.name);
|
||||
fresh.urls = paired.map(p => p.url);
|
||||
fresh.visibleIdx = fresh.names.map((_, i) => i).filter(i => !fileHidden[fresh.names[i]]);
|
||||
}
|
||||
|
||||
// Re-open picker to reflect new order
|
||||
showVariantPicker(filename);
|
||||
// Poll for the freshly-extracted face crop (async on the server).
|
||||
showVariantPicker(gid);
|
||||
loadFaceThumb(gid, 8);
|
||||
setTimeout(refreshNow, 2000);
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
@@ -12037,6 +12033,9 @@
|
||||
// Refresh UI as each task completes; auto-follow to newest image
|
||||
if (job.done > prevDone) {
|
||||
prevDone = job.done;
|
||||
if (job.latest_output) {
|
||||
_followLatestFilename = job.latest_output;
|
||||
}
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow(true);
|
||||
}
|
||||
@@ -12057,6 +12056,9 @@
|
||||
_sbSelectedAngles.clear();
|
||||
_sbPoseEdits = {};
|
||||
_sbGenJobId = null;
|
||||
if (job.latest_output) {
|
||||
_followLatestFilename = job.latest_output;
|
||||
}
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow(true);
|
||||
renderSidebarGenerate();
|
||||
@@ -13150,10 +13152,43 @@
|
||||
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();
|
||||
|
||||
const newFn = d.nobg_filename;
|
||||
const nobgUrl = IMAGE_FOLDER + newFn + '?t=' + Date.now();
|
||||
_nobgSidecars.set(fname, nobgUrl);
|
||||
|
||||
// Clone metadata locally to avoid refresh blink
|
||||
filePrompts[newFn] = filePrompts[fname];
|
||||
filePoses[newFn] = (filePoses[fname] || '') + ' (No BG)';
|
||||
fileHasBg[newFn] = false;
|
||||
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||
fileContentType[newFn] = 'image';
|
||||
fileSourceRefs[newFn] = [fname];
|
||||
fileHidden[newFn] = false;
|
||||
fileIsSource[newFn] = false;
|
||||
fileArchived[newFn] = false;
|
||||
|
||||
// Insert the sidecar right after the current image in the studio strip
|
||||
lbUrls.splice(lbIdx + 1, 0, nobgUrl);
|
||||
lbNames.splice(lbIdx + 1, 0, newFn);
|
||||
|
||||
const group = groupData.get(lbCurrentGid);
|
||||
if (group) {
|
||||
const gIdx = group.names.indexOf(fname);
|
||||
if (gIdx !== -1) {
|
||||
group.urls.splice(gIdx + 1, 0, nobgUrl);
|
||||
group.names.splice(gIdx + 1, 0, newFn);
|
||||
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the new no-bg image
|
||||
lbIdx = lbIdx + 1;
|
||||
updateStudio();
|
||||
_followLatestFilename = newFn;
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow(true);
|
||||
|
||||
// Preload before swap to avoid black flash
|
||||
const preload = new Image();
|
||||
preload.onload = () => {
|
||||
@@ -13197,16 +13232,10 @@
|
||||
const d = await r.json();
|
||||
if (statusEl) statusEl.textContent = 'Done (rembg)';
|
||||
showToast('Background removed (rembg)', 'success');
|
||||
fileHasBg[fname] = false;
|
||||
_bustStudioImage(fname);
|
||||
toggleCheckerboard(true);
|
||||
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;
|
||||
toggleCheckerboard(true);
|
||||
};
|
||||
preload.src = nobgUrl;
|
||||
}
|
||||
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
|
||||
if (btn) btn.disabled = false;
|
||||
@@ -13248,25 +13277,42 @@
|
||||
try {
|
||||
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/set-preferred`, {method:'POST'});
|
||||
if (!r.ok) return;
|
||||
|
||||
// Update sort orders locally to match backend logic
|
||||
const gid = lbCurrentGid;
|
||||
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
|
||||
const fresh = groupData.get(gid);
|
||||
if (fresh) {
|
||||
fresh.names.forEach(n => {
|
||||
if (n !== fname) {
|
||||
fileSortOrders[n] = (fileSortOrders[n] || 0) + 1;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
// Re-sort local group data
|
||||
const paired = fresh.names.map((name, i) => ({ name, url: fresh.urls[i] }));
|
||||
paired.sort((a, b) => {
|
||||
const oa = fileSortOrders[a.name] ?? 9999;
|
||||
const ob = fileSortOrders[b.name] ?? 9999;
|
||||
if (oa !== ob) return oa - ob;
|
||||
return a.name.length - b.name.length;
|
||||
});
|
||||
fresh.names = paired.map(p => p.name);
|
||||
fresh.urls = paired.map(p => p.url);
|
||||
fresh.visibleIdx = fresh.names.map((_, i) => i).filter(i => !fileHidden[fresh.names[i]]);
|
||||
|
||||
lbUrls = fresh.urls;
|
||||
lbNames = fresh.names;
|
||||
lbIdx = lbNames.indexOf(fname);
|
||||
}
|
||||
|
||||
updateStudio();
|
||||
showToast('Set as preferred', 'success');
|
||||
|
||||
// Delayed refresh to reconcile with server without disrupting local state
|
||||
setTimeout(refreshNow, 2000);
|
||||
|
||||
// Poll for the freshly-extracted face crop (async on the server).
|
||||
loadFaceThumb(gid, 8);
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,43 @@ IDLE_THRESHOLD = 45 # seconds of inactivity before background gen starts
|
||||
IDLE_CHECK_INTERVAL = 4 # polling interval (seconds)
|
||||
|
||||
|
||||
# --- File Metadata In-Memory Cache (resolves performance bottlenecks on /mnt/zim) ---
|
||||
_file_meta_cache = {} # filename -> (exists, mtime, cache_time)
|
||||
_file_meta_cache_lock = threading.Lock()
|
||||
|
||||
def _get_cached_file_meta(filename: str, output_dir: str):
|
||||
now = time.time()
|
||||
with _file_meta_cache_lock:
|
||||
cached = _file_meta_cache.get(filename)
|
||||
if cached and (now - cached[2] < 5.0):
|
||||
return cached[0], cached[1]
|
||||
|
||||
fpath = os.path.join(output_dir, filename)
|
||||
exists = os.path.exists(fpath)
|
||||
mtime = 0.0
|
||||
if exists:
|
||||
try:
|
||||
mtime = os.path.getmtime(fpath)
|
||||
except Exception:
|
||||
pass
|
||||
with _file_meta_cache_lock:
|
||||
_file_meta_cache[filename] = (exists, mtime, now)
|
||||
return exists, mtime
|
||||
|
||||
def _update_cached_file_meta(filename: str, exists: bool = True, mtime: float = None):
|
||||
if mtime is None:
|
||||
mtime = time.time()
|
||||
with _file_meta_cache_lock:
|
||||
_file_meta_cache[filename] = (exists, mtime, time.time())
|
||||
|
||||
def _clear_cached_file_meta(filename: str):
|
||||
with _file_meta_cache_lock:
|
||||
if filename in _file_meta_cache:
|
||||
del _file_meta_cache[filename]
|
||||
|
||||
|
||||
_last_preloaded_images_set = None
|
||||
|
||||
@app.middleware("http")
|
||||
async def _track_activity(request, call_next):
|
||||
global _last_request_time
|
||||
@@ -126,6 +163,7 @@ async def _track_activity(request, call_next):
|
||||
|
||||
def _sync_preloaded_images():
|
||||
"""Update PRELOADED_IMAGES in car.html (both source and output) to match current DB state."""
|
||||
global _last_preloaded_images_set
|
||||
try:
|
||||
output_dir = _load_output_dir()
|
||||
paths = [
|
||||
@@ -134,11 +172,21 @@ def _sync_preloaded_images():
|
||||
]
|
||||
|
||||
persons = database.list_persons(include_archived=False)
|
||||
# Only include if file actually exists on disk
|
||||
db_images = [p[0] for p in persons if os.path.exists(os.path.join(output_dir, p[0]))]
|
||||
# Only include if file actually exists on disk, using the fast cache
|
||||
db_images = []
|
||||
for p in persons:
|
||||
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||
if exists:
|
||||
db_images.append(p[0])
|
||||
|
||||
# Sort by mtime, newest first
|
||||
db_images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
db_images.sort(key=lambda x: _get_cached_file_meta(x, output_dir)[1], reverse=True)
|
||||
|
||||
# Avoid redundant HTML rewrites if the preloaded set is unchanged
|
||||
current_set = set(db_images)
|
||||
if _last_preloaded_images_set is not None and _last_preloaded_images_set == current_set:
|
||||
return
|
||||
_last_preloaded_images_set = current_set
|
||||
|
||||
images_json = json.dumps(db_images, indent=12).strip()
|
||||
# Format for JS insertion
|
||||
@@ -195,7 +243,8 @@ def _watch_frontend():
|
||||
def _load_wireframe_dir() -> str:
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
conf = json.load(f)
|
||||
return conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
||||
d = conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
||||
return os.path.expanduser(d)
|
||||
|
||||
|
||||
def _load_faceswap_model_path() -> str:
|
||||
@@ -1055,9 +1104,9 @@ def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
|
||||
source_refs=json.dumps([model_filename]),
|
||||
sort_order=database.get_next_sort_order(group_id),
|
||||
)
|
||||
_invalidate_static()
|
||||
jobs[job_id]['status'] = 'done'
|
||||
jobs[job_id]['output'] = out_name
|
||||
_invalidate_static()
|
||||
|
||||
except Exception as e:
|
||||
print(f'[faceswap-ff] error: {e}')
|
||||
@@ -1270,7 +1319,7 @@ jobs: dict[str, dict] = {}
|
||||
def _load_output_dir() -> str:
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
conf = json.load(f)
|
||||
d = conf["output_dir"]
|
||||
d = os.path.expanduser(conf["output_dir"])
|
||||
if not os.path.isabs(d):
|
||||
d = os.path.normpath(os.path.join(os.path.dirname(CONFIG_PATH), "..", d))
|
||||
return d
|
||||
@@ -1363,8 +1412,8 @@ def _write_all_static() -> None:
|
||||
db_images = []
|
||||
archived_count = 0
|
||||
for p in persons:
|
||||
fpath = os.path.join(output_dir, p[0])
|
||||
if not os.path.exists(fpath):
|
||||
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||
if not exists:
|
||||
continue
|
||||
|
||||
is_archived = bool(p[14]) if p[14] else False
|
||||
@@ -1406,7 +1455,7 @@ def _write_all_static() -> None:
|
||||
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
|
||||
try:
|
||||
db_images.sort(
|
||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
||||
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||
reverse=True,
|
||||
)
|
||||
except Exception:
|
||||
@@ -1705,6 +1754,7 @@ def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
|
||||
except Exception as db_err:
|
||||
print(f"Database error in batch worker: {db_err}")
|
||||
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["done"] += 1
|
||||
# Regenerate static JSON so the frontend's polling picks up the
|
||||
# new image immediately (progressive refresh, not just at the end).
|
||||
@@ -1806,6 +1856,7 @@ def _multi_ref_worker(job_id: str, filenames: list[str], prompts: list[str], pos
|
||||
except Exception as db_err:
|
||||
print(f"DB error in multi-ref: {db_err}")
|
||||
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["done"] += 1
|
||||
# Regenerate static JSON so the frontend's polling picks up the new
|
||||
# image immediately (progressive refresh, matching _batch_worker).
|
||||
@@ -2044,8 +2095,8 @@ def list_images(archived: bool = False):
|
||||
# source_refs, has_clothing, content_type, faceswap_source_video, archived
|
||||
db_images = []
|
||||
for p in persons:
|
||||
fpath = os.path.join(output_dir, p[0])
|
||||
if not os.path.exists(fpath):
|
||||
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||
if not exists:
|
||||
continue
|
||||
db_images.append({
|
||||
"filename": p[0],
|
||||
@@ -2065,7 +2116,7 @@ def list_images(archived: bool = False):
|
||||
"archived": bool(p[14]) if p[14] else False,
|
||||
})
|
||||
db_images.sort(
|
||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
||||
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||
reverse=True,
|
||||
)
|
||||
return {"images": db_images}
|
||||
@@ -2079,7 +2130,7 @@ def list_images(archived: bool = False):
|
||||
if f.lower().endswith(all_extensions)
|
||||
and not (f.lower().endswith('.jpg') and os.path.splitext(f)[0] in video_stems)
|
||||
]
|
||||
files.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
files.sort(key=lambda x: _get_cached_file_meta(x, output_dir)[1], reverse=True)
|
||||
return {"images": [{"filename": f} for f in files]}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
@@ -2568,7 +2619,7 @@ def merge_groups(req: MergeRequest):
|
||||
|
||||
# Write synchronously: the frontend reloads images.json immediately after this
|
||||
# returns, so an async rebuild would race and show the pre-merge grouping.
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"group_id": gid, "files": req.filenames}
|
||||
|
||||
|
||||
@@ -2584,7 +2635,7 @@ def extract_from_group(req: ExtractRequest):
|
||||
except Exception as db_err:
|
||||
print(f"Database error in extract: {db_err}")
|
||||
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"filename": req.filename}
|
||||
|
||||
|
||||
@@ -2762,7 +2813,7 @@ def set_privacy_lock():
|
||||
global _privacy_locked
|
||||
with _privacy_lock:
|
||||
_privacy_locked = True
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"status": "locked"}
|
||||
|
||||
|
||||
@@ -2771,7 +2822,7 @@ def set_privacy_unlock():
|
||||
global _privacy_locked
|
||||
with _privacy_lock:
|
||||
_privacy_locked = False
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"status": "unlocked"}
|
||||
|
||||
|
||||
@@ -2827,7 +2878,8 @@ def _extract_face_bg(filename: str, fpath: str):
|
||||
group_id = person[1] if person else None
|
||||
gid_tag = (group_id or "face").replace("/", "_")
|
||||
face_fname = f"{gid_tag}_face.png"
|
||||
face_path = os.path.join(os.path.dirname(fpath), face_fname)
|
||||
output_dir = _load_output_dir()
|
||||
face_path = os.path.join(output_dir, face_fname)
|
||||
cropped.save(face_path)
|
||||
face_embed = face.normed_embedding.tolist() if hasattr(face, 'normed_embedding') and face.normed_embedding is not None else None
|
||||
database.upsert_person(face_fname, filepath=face_path, group_id=group_id,
|
||||
@@ -2837,6 +2889,7 @@ def _extract_face_bg(filename: str, fpath: str):
|
||||
hidden=True,
|
||||
tags=["FACE"])
|
||||
print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
|
||||
_invalidate_static()
|
||||
except Exception as e:
|
||||
print(f"[extract-face] error for {filename}: {e}")
|
||||
|
||||
@@ -2933,6 +2986,9 @@ def upload_image(
|
||||
shutil.copyfileobj(image.file, f)
|
||||
|
||||
# Fast path: add to existing group without pose generation
|
||||
if not group_id:
|
||||
group_id = naming.get_base_name(filename)
|
||||
|
||||
if group_id and skip_poses:
|
||||
sort_order = database.get_next_sort_order(group_id)
|
||||
database.upsert_person(filename, filepath=file_path, group_id=group_id,
|
||||
@@ -3055,8 +3111,9 @@ def set_image_preferred(filename: str):
|
||||
others = [r[0] for r in rows if r[0] != filename]
|
||||
database.set_group_order(group_id, [filename] + others)
|
||||
_invalidate_static()
|
||||
fpath = os.path.join(_load_output_dir(), filename)
|
||||
if os.path.exists(fpath):
|
||||
# Use stored filepath if available (absolute), else resolve relative to output_dir
|
||||
fpath = person[5] if (len(person) > 5 and person[5]) else os.path.join(_load_output_dir(), filename)
|
||||
if fpath and os.path.exists(fpath):
|
||||
_face_executor.submit(_extract_face_bg, filename, fpath)
|
||||
return {"filename": filename, "group_id": group_id}
|
||||
|
||||
@@ -3064,9 +3121,9 @@ def set_image_preferred(filename: str):
|
||||
@app.post("/images/{filename:path}/extract-face")
|
||||
def extract_face_endpoint(filename: str):
|
||||
"""Detect and crop the largest face from image; saves as {group_id}_face.png."""
|
||||
output_dir = _load_output_dir()
|
||||
fpath = os.path.join(output_dir, filename)
|
||||
if not os.path.exists(fpath):
|
||||
person = database.get_person(filename)
|
||||
fpath = person[5] if (person and len(person) > 5 and person[5]) else os.path.join(_load_output_dir(), filename)
|
||||
if not fpath or not os.path.exists(fpath):
|
||||
raise HTTPException(404, "not found")
|
||||
_face_executor.submit(_extract_face_bg, filename, fpath)
|
||||
return {"status": "queued", "filename": filename}
|
||||
@@ -3506,6 +3563,7 @@ def delete_image(filename: str):
|
||||
_move_to_trash(person[5])
|
||||
|
||||
database.delete_person(filename)
|
||||
_update_cached_file_meta(filename, exists=False)
|
||||
_invalidate_static()
|
||||
return {"status": "deleted", "filename": filename}
|
||||
|
||||
@@ -3563,6 +3621,7 @@ def delete_group(group_id: str):
|
||||
for filename, filepath in files:
|
||||
if filepath and os.path.exists(filepath):
|
||||
_move_to_trash(filepath)
|
||||
_update_cached_file_meta(filename, exists=False)
|
||||
|
||||
database.delete_group(group_id)
|
||||
_invalidate_static()
|
||||
@@ -3748,6 +3807,7 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||
source_refs=json.dumps(refs))
|
||||
except Exception as db_err:
|
||||
print(f"[scenery] DB error: {db_err}")
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["status"] = "done"
|
||||
jobs[job_id]["output"] = out_name
|
||||
# Regenerate the static JSON so the frontend's polling surfaces the new
|
||||
@@ -4182,11 +4242,27 @@ def remove_background_sam(filename: str):
|
||||
f.write(transparent_png)
|
||||
|
||||
# Register sidecar in DB so it appears in the same group
|
||||
group_id = person[1]
|
||||
database.upsert_person(nobg_filename, filepath=nobg_path,
|
||||
group_id=group_id, has_background=False,
|
||||
source_refs=json.dumps([filename]))
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
database.upsert_person(
|
||||
nobg_filename, filepath=nobg_path, group_id=group_id,
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=False,
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]), # original is the reference
|
||||
)
|
||||
|
||||
_invalidate_static()
|
||||
used_sam2 = _sam2_predictor is not False and _sam2_predictor is not None
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -4258,10 +4334,23 @@ def manual_crop_image(filename: str, req: CropRequest):
|
||||
new_filename = new_basename
|
||||
path = os.path.join(output_dir, new_basename)
|
||||
shutil.copy2(src_path, path)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
database.upsert_person(
|
||||
new_filename, filepath=path, group_id=person[1],
|
||||
prompt=person[6], pose=person[7],
|
||||
has_background=person[11], has_clothing=person[13],
|
||||
new_filename, filepath=path, group_id=group_id,
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=person[11],
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]), # original is the reference
|
||||
)
|
||||
else:
|
||||
@@ -4376,10 +4465,23 @@ def pad_image(filename: str, req: PadRequest):
|
||||
new_filename = new_basename
|
||||
path = os.path.join(output_dir, new_basename)
|
||||
shutil.copy2(src_path, path)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
database.upsert_person(
|
||||
new_filename, filepath=path, group_id=person[1],
|
||||
prompt=person[6], pose=person[7],
|
||||
has_background=person[11], has_clothing=person[13],
|
||||
new_filename, filepath=path, group_id=group_id,
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=person[11],
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]),
|
||||
)
|
||||
else:
|
||||
@@ -4393,7 +4495,20 @@ def pad_image(filename: str, req: PadRequest):
|
||||
|
||||
if req.outpaint:
|
||||
# Trigger Qwen outpainting
|
||||
out_instr = "Naturally outpaint and extend the borders of the image to complete the scene."
|
||||
# If the image is transparent, composite it onto a neutral background (black)
|
||||
# so the AI can see the area to be filled.
|
||||
qwen_input = padded
|
||||
fill_desc = ""
|
||||
if qwen_input.mode == "RGBA":
|
||||
# Composite onto black for maximal contrast in outpainting
|
||||
bg = Image.new("RGB", qwen_input.size, (0, 0, 0))
|
||||
bg.paste(qwen_input, mask=qwen_input.split()[3])
|
||||
qwen_input = bg
|
||||
fill_desc = " replacing the black background areas"
|
||||
elif req.fill in ["black", "white"]:
|
||||
fill_desc = f" replacing the {req.fill} background areas"
|
||||
|
||||
out_instr = f"Naturally outpaint and extend the borders of the image to complete the scene{fill_desc}."
|
||||
actual_prompt = req.prompt or out_instr
|
||||
if req.prompt and out_instr.lower() not in actual_prompt.lower():
|
||||
actual_prompt = f"{actual_prompt}. {out_instr}"
|
||||
@@ -4401,7 +4516,7 @@ def pad_image(filename: str, req: PadRequest):
|
||||
actual_prompt = f"{person[6]}. {out_instr}"
|
||||
|
||||
try:
|
||||
png_bytes = _run_pipeline(padded, actual_prompt)
|
||||
png_bytes = _run_pipeline(qwen_input, actual_prompt)
|
||||
padded = Image.open(io.BytesIO(png_bytes))
|
||||
# Register the outpaint prompt and set has_background=True in DB
|
||||
database.upsert_person(new_filename, prompt=actual_prompt, has_background=True)
|
||||
@@ -4474,16 +4589,29 @@ def duplicate_image(filename: str):
|
||||
|
||||
new_path = os.path.join(output_dir, new_basename)
|
||||
_shutil.copy2(path, new_path)
|
||||
group_id = person[1]
|
||||
# person tuple: (name, group_id, tags, embedding, clip_description, filepath,
|
||||
# prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
|
||||
# Parse tags list if present
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
|
||||
database.upsert_person(
|
||||
new_filename, filepath=new_path, group_id=group_id,
|
||||
prompt=person[6], pose=person[7],
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=person[11],
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]), # original is the reference
|
||||
)
|
||||
_update_cached_file_meta(new_filename, exists=True)
|
||||
_invalidate_static()
|
||||
return {"status": "success", "new_filename": new_filename, "new_url": f"/output/{new_filename}"}
|
||||
|
||||
@@ -4503,6 +4631,7 @@ def restore_background(filename: str):
|
||||
bg.save(buf, format="PNG")
|
||||
with open(path, "wb") as f:
|
||||
f.write(buf.getvalue())
|
||||
_invalidate_static()
|
||||
return {"status": "success", "filename": filename}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ Anatomically precise, photo realistic, high detail, keep the characteristics of
|
||||
|
||||
Masterpiece, you are in a black void, hyper-realistic, high quality. detailed. detailed skin.
|
||||
|
||||
# Outpaint legs
|
||||
|
||||
Naturally outpaint draw the legs and feet, and arms of both female keeping the full image intacts.
|
||||
|
||||
# Feminine
|
||||
|
||||
1 female nude.
|
||||
@@ -36,7 +40,6 @@ Keep characteristics of person in Image 2
|
||||
|
||||
replace the female in Image 1 by the teenage female in Image 2. Keep the Identical person in image 2. photo realistic
|
||||
|
||||
|
||||
# Undress
|
||||
|
||||
masterpiece, high quality,
|
||||
@@ -4082,11 +4085,21 @@ The body is fully exposed for inspection.
|
||||
Eyes looking at the camera, facial characteristics as reference.
|
||||
Anatomically precise, hyperrealistic, high detail, keep the characteristics of the reference image.
|
||||
|
||||
# Labial Lattice 2hand
|
||||
|
||||
seated, legs spread wide, one leg extended forward, the other bent at the knee with foot resting flat.
|
||||
torso leaning slightly back.
|
||||
arms extended along the thighs, fingers lightly brushing labia.
|
||||
head tilted back, eyes closed, a subtle smile on the lips.
|
||||
legs positioned to create a triangular frame around the genitals.
|
||||
a sense of vulnerability and openness.
|
||||
Anatomically precise, hyperrealistic, high detail, keep the characteristics of the reference image.
|
||||
|
||||
# Labial Lattice
|
||||
|
||||
seated, legs spread wide, one leg extended forward, the other bent at the knee with foot resting flat.
|
||||
torso leaning slightly back, supported on hands placed beside hips.
|
||||
arms extended along the thighs, fingers lightly brushing labia.
|
||||
torso leaning slightly back, supported on one hand placed beside hips.
|
||||
one arm extended along the thighs, fingers lightly brushing labia.
|
||||
head tilted back, eyes closed, a subtle smile on the lips.
|
||||
legs positioned to create a triangular frame around the genitals.
|
||||
a sense of vulnerability and openness.
|
||||
|
||||
394
tour-comfy/test_regression_api.py
Normal file
394
tour-comfy/test_regression_api.py
Normal file
@@ -0,0 +1,394 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import unittest
|
||||
from datetime import datetime as _dt
|
||||
from PIL import Image
|
||||
|
||||
# Ensure tour-comfy is in import path so we can import the FastAPI app and database module
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
import database
|
||||
from edit_api import app, _load_output_dir
|
||||
|
||||
class TestAPIRegression(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Determine output directory
|
||||
cls.output_dir = _load_output_dir()
|
||||
cls.client = TestClient(app)
|
||||
|
||||
# Unique mock identifiers to avoid conflicts
|
||||
cls.test_ref_filename = "test_regression_ref_image_123.png"
|
||||
cls.test_other_filename = "test_regression_other_image_123.png"
|
||||
cls.group_id = "test_regression_group_123"
|
||||
cls.test_ref_path = os.path.join(cls.output_dir, cls.test_ref_filename)
|
||||
cls.test_other_path = os.path.join(cls.output_dir, cls.test_other_filename)
|
||||
|
||||
# Ensure any leftover artifacts from past failed runs are cleaned
|
||||
cls._cleanup_database()
|
||||
cls._cleanup_files()
|
||||
|
||||
# Create dummy test files (100x100 pixels, RGB format)
|
||||
img = Image.new("RGB", (100, 100), color=(255, 0, 0))
|
||||
img.save(cls.test_ref_path, "PNG")
|
||||
|
||||
img_other = Image.new("RGB", (100, 100), color=(0, 255, 0))
|
||||
img_other.save(cls.test_other_path, "PNG")
|
||||
|
||||
# Mock metadata
|
||||
cls.name = "Regression Test Character"
|
||||
cls.tags = ["VISIBLE", "LIKE", "21+"]
|
||||
cls.embedding = [0.1] * 1024
|
||||
cls.clip_description = "A dummy regression test image description"
|
||||
cls.prompt = "masterpiece, high quality"
|
||||
cls.pose = "standing"
|
||||
cls.group_name = "Regression Group"
|
||||
cls.hidden = False
|
||||
cls.has_background = True
|
||||
cls.has_clothing = False
|
||||
cls.is_source = True
|
||||
cls.pose_description = "The model is standing and looking directly at the camera."
|
||||
cls.pose_skeleton = '{"keypoints": [1, 2, 3]}'
|
||||
|
||||
# Insert reference image into the database
|
||||
database.upsert_person(
|
||||
cls.test_ref_filename,
|
||||
filepath=cls.test_ref_path,
|
||||
name=cls.name,
|
||||
group_id=cls.group_id,
|
||||
tags=cls.tags,
|
||||
embedding=cls.embedding,
|
||||
clip_description=cls.clip_description,
|
||||
prompt=cls.prompt,
|
||||
pose=cls.pose,
|
||||
sort_order=0,
|
||||
group_name=cls.group_name,
|
||||
hidden=cls.hidden,
|
||||
has_background=cls.has_background,
|
||||
has_clothing=cls.has_clothing,
|
||||
is_source=cls.is_source,
|
||||
pose_description=cls.pose_description,
|
||||
pose_skeleton=cls.pose_skeleton
|
||||
)
|
||||
|
||||
# Insert second image in same group for reordering test
|
||||
database.upsert_person(
|
||||
cls.test_other_filename,
|
||||
filepath=cls.test_other_path,
|
||||
name=cls.name,
|
||||
group_id=cls.group_id,
|
||||
tags=cls.tags,
|
||||
embedding=cls.embedding,
|
||||
clip_description=cls.clip_description,
|
||||
prompt=cls.prompt,
|
||||
pose=cls.pose,
|
||||
sort_order=1,
|
||||
group_name=cls.group_name,
|
||||
hidden=cls.hidden,
|
||||
has_background=cls.has_background,
|
||||
has_clothing=cls.has_clothing,
|
||||
is_source=cls.is_source,
|
||||
pose_description=cls.pose_description,
|
||||
pose_skeleton=cls.pose_skeleton
|
||||
)
|
||||
|
||||
# Track dynamically created files and database rows for cleanup
|
||||
cls.created_files = [cls.test_ref_path, cls.test_other_path]
|
||||
cls.created_db_rows = [cls.test_ref_filename, cls.test_other_filename]
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Cleanup
|
||||
cls._cleanup_database()
|
||||
cls._cleanup_files()
|
||||
|
||||
@classmethod
|
||||
def _cleanup_database(cls):
|
||||
# Safely delete any inserted test rows from the person database table
|
||||
conn = database.get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
# Delete any filenames starting with test_regression or containing ts_crop/ts_pad
|
||||
cur.execute("""
|
||||
DELETE FROM person
|
||||
WHERE filename LIKE 'test_regression_%%'
|
||||
OR filename LIKE '%%_crop_test_regression_%%'
|
||||
OR filename LIKE '%%_pad_test_regression_%%'
|
||||
OR group_id = %s
|
||||
""", (cls.group_id,))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f"Error cleaning up database: {e}")
|
||||
conn.rollback()
|
||||
finally:
|
||||
cur.close()
|
||||
database._put_db_connection(conn)
|
||||
|
||||
@classmethod
|
||||
def _cleanup_files(cls):
|
||||
# Clean up files matching test patterns
|
||||
for f in os.listdir(cls.output_dir):
|
||||
if "test_regression_" in f or "_crop_" in f or "_pad_" in f:
|
||||
p = os.path.join(cls.output_dir, f)
|
||||
try:
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
except Exception as e:
|
||||
print(f"Error removing file {p}: {e}")
|
||||
|
||||
def assertEmbeddingEqual(self, val1, val2):
|
||||
if isinstance(val1, str):
|
||||
val1 = [float(x) for x in val1.strip("[]").split(",")]
|
||||
if isinstance(val2, str):
|
||||
val2 = [float(x) for x in val2.strip("[]").split(",")]
|
||||
self.assertEqual(len(val1), len(val2))
|
||||
for x, y in zip(val1, val2):
|
||||
self.assertAlmostEqual(x, y, places=4)
|
||||
|
||||
def test_01_duplicate_copies_all_pose_and_meta_details(self):
|
||||
# Send duplicate request to FastAPI
|
||||
response = self.client.post(f"/images/{self.test_ref_filename}/duplicate")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_data = response.json()
|
||||
self.assertEqual(res_data["status"], "success")
|
||||
|
||||
new_filename = res_data["new_filename"]
|
||||
self.created_db_rows.append(new_filename)
|
||||
new_path = os.path.join(self.output_dir, os.path.basename(new_filename))
|
||||
self.created_files.append(new_path)
|
||||
|
||||
# Assert duplicate file exists on disk
|
||||
self.assertTrue(os.path.exists(new_path), f"Duplicated file {new_path} not found on disk")
|
||||
|
||||
# Assert all metadata has been accurately duplicated in DB
|
||||
person = database.get_person(new_filename)
|
||||
self.assertIsNotNone(person, "Duplicated database entry not found")
|
||||
|
||||
# Column mappings as in database.py get_person:
|
||||
# SELECT name, group_id, tags, embedding, clip_description, filepath,
|
||||
# prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
||||
# has_clothing, is_source, pose_description, pose_skeleton
|
||||
self.assertEqual(person[0], self.name)
|
||||
self.assertEqual(person[1], self.group_id)
|
||||
|
||||
# Tags (assert LIKE and 21+ are preserved)
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
self.assertIn("LIKE", tags_list)
|
||||
self.assertIn("21+", tags_list)
|
||||
self.assertIn("VISIBLE", tags_list)
|
||||
|
||||
# Verify 21+ is just a standard tag, not a safety blocker
|
||||
self.assertIn("21+", tags_list)
|
||||
|
||||
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||
self.assertEqual(person[4], self.clip_description)
|
||||
self.assertEqual(person[5], new_path)
|
||||
self.assertEqual(person[6], self.prompt)
|
||||
self.assertEqual(person[7], self.pose)
|
||||
self.assertEqual(person[9], self.group_name)
|
||||
self.assertEqual(person[10], self.hidden)
|
||||
self.assertEqual(person[11], self.has_background)
|
||||
|
||||
# source_refs should refer to original
|
||||
source_refs = json.loads(person[12]) if isinstance(person[12], str) else person[12]
|
||||
self.assertIn(self.test_ref_filename, source_refs)
|
||||
|
||||
self.assertEqual(person[13], self.has_clothing)
|
||||
self.assertEqual(person[14], self.is_source)
|
||||
self.assertEqual(person[15], self.pose_description)
|
||||
self.assertEqual(person[16], self.pose_skeleton)
|
||||
|
||||
def test_02_crop_as_copy_copies_all_pose_and_meta_details(self):
|
||||
# Crop region: (10, 10, 90, 90), as_copy=True
|
||||
req_payload = {
|
||||
"x1": 10,
|
||||
"y1": 10,
|
||||
"x2": 90,
|
||||
"y2": 90,
|
||||
"as_copy": True
|
||||
}
|
||||
response = self.client.post(f"/images/{self.test_ref_filename}/crop", json=req_payload)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_data = response.json()
|
||||
self.assertEqual(res_data["status"], "success")
|
||||
|
||||
new_filename = res_data["new_filename"]
|
||||
self.created_db_rows.append(new_filename)
|
||||
new_path = os.path.join(self.output_dir, os.path.basename(new_filename))
|
||||
self.created_files.append(new_path)
|
||||
|
||||
# Verify physical file existence and crop dimensions (should be 80x80)
|
||||
self.assertTrue(os.path.exists(new_path), f"Cropped file {new_path} not found on disk")
|
||||
cropped_img = Image.open(new_path)
|
||||
self.assertEqual(cropped_img.size, (80, 80))
|
||||
|
||||
# Verify database entry has complete metadata
|
||||
person = database.get_person(new_filename)
|
||||
self.assertIsNotNone(person, "Cropped copy database entry not found")
|
||||
|
||||
self.assertEqual(person[0], self.name)
|
||||
self.assertEqual(person[1], self.group_id)
|
||||
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
self.assertIn("LIKE", tags_list)
|
||||
self.assertIn("21+", tags_list)
|
||||
self.assertIn("VISIBLE", tags_list)
|
||||
|
||||
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||
self.assertEqual(person[4], self.clip_description)
|
||||
self.assertEqual(person[5], new_path)
|
||||
self.assertEqual(person[6], self.prompt)
|
||||
self.assertEqual(person[7], self.pose)
|
||||
self.assertEqual(person[9], self.group_name)
|
||||
self.assertEqual(person[10], self.hidden)
|
||||
self.assertEqual(person[11], self.has_background)
|
||||
|
||||
source_refs = json.loads(person[12]) if isinstance(person[12], str) else person[12]
|
||||
self.assertIn(self.test_ref_filename, source_refs)
|
||||
|
||||
self.assertEqual(person[13], self.has_clothing)
|
||||
self.assertEqual(person[14], self.is_source)
|
||||
self.assertEqual(person[15], self.pose_description)
|
||||
self.assertEqual(person[16], self.pose_skeleton)
|
||||
|
||||
def test_03_crop_in_place_keeps_all_meta_information(self):
|
||||
# Crop region: (20, 20, 80, 80), as_copy=False (in-place)
|
||||
req_payload = {
|
||||
"x1": 20,
|
||||
"y1": 20,
|
||||
"x2": 80,
|
||||
"y2": 80,
|
||||
"as_copy": False
|
||||
}
|
||||
|
||||
# First verify original size is 100x100
|
||||
orig_img = Image.open(self.test_ref_path)
|
||||
self.assertEqual(orig_img.size, (100, 100))
|
||||
|
||||
response = self.client.post(f"/images/{self.test_ref_filename}/crop", json=req_payload)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_data = response.json()
|
||||
self.assertEqual(res_data["status"], "success")
|
||||
self.assertEqual(res_data["new_filename"], self.test_ref_filename)
|
||||
|
||||
# Verify physical file size updated to 60x60
|
||||
cropped_img = Image.open(self.test_ref_path)
|
||||
self.assertEqual(cropped_img.size, (60, 60))
|
||||
|
||||
# Verify database entry has complete metadata untouched
|
||||
person = database.get_person(self.test_ref_filename)
|
||||
self.assertIsNotNone(person, "Database entry not found after in-place crop")
|
||||
|
||||
self.assertEqual(person[0], self.name)
|
||||
self.assertEqual(person[1], self.group_id)
|
||||
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
self.assertIn("LIKE", tags_list)
|
||||
self.assertIn("21+", tags_list)
|
||||
|
||||
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||
self.assertEqual(person[4], self.clip_description)
|
||||
self.assertEqual(person[6], self.prompt)
|
||||
self.assertEqual(person[7], self.pose)
|
||||
self.assertEqual(person[9], self.group_name)
|
||||
self.assertEqual(person[10], self.hidden)
|
||||
self.assertEqual(person[11], self.has_background)
|
||||
self.assertEqual(person[13], self.has_clothing)
|
||||
self.assertEqual(person[14], self.is_source)
|
||||
self.assertEqual(person[15], self.pose_description)
|
||||
self.assertEqual(person[16], self.pose_skeleton)
|
||||
|
||||
def test_04_pad_as_copy_copies_all_pose_and_meta_details(self):
|
||||
# Pad canvas: expand top and bottom by 10 pixels, as_copy=True
|
||||
req_payload = {
|
||||
"top": 10,
|
||||
"right": 0,
|
||||
"bottom": 10,
|
||||
"left": 0,
|
||||
"as_copy": True,
|
||||
"fill": "transparent",
|
||||
"outpaint": False
|
||||
}
|
||||
# Original size at this point is 60x60 (due to test_03 crop)
|
||||
response = self.client.post(f"/images/{self.test_ref_filename}/pad", json=req_payload)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_data = response.json()
|
||||
self.assertEqual(res_data["status"], "success")
|
||||
|
||||
new_filename = res_data["new_filename"]
|
||||
self.created_db_rows.append(new_filename)
|
||||
new_path = os.path.join(self.output_dir, os.path.basename(new_filename))
|
||||
self.created_files.append(new_path)
|
||||
|
||||
# Verify physical file existence and pad dimensions (should be 60x80)
|
||||
self.assertTrue(os.path.exists(new_path), f"Padded file {new_path} not found on disk")
|
||||
padded_img = Image.open(new_path)
|
||||
self.assertEqual(padded_img.size, (60, 80))
|
||||
|
||||
# Verify database entry has complete metadata
|
||||
person = database.get_person(new_filename)
|
||||
self.assertIsNotNone(person, "Padded copy database entry not found")
|
||||
|
||||
self.assertEqual(person[0], self.name)
|
||||
self.assertEqual(person[1], self.group_id)
|
||||
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
self.assertIn("LIKE", tags_list)
|
||||
self.assertIn("21+", tags_list)
|
||||
self.assertIn("VISIBLE", tags_list)
|
||||
|
||||
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||
self.assertEqual(person[4], self.clip_description)
|
||||
self.assertEqual(person[5], new_path)
|
||||
self.assertEqual(person[6], self.prompt)
|
||||
self.assertEqual(person[7], self.pose)
|
||||
self.assertEqual(person[9], self.group_name)
|
||||
self.assertEqual(person[10], self.hidden)
|
||||
self.assertEqual(person[11], self.has_background)
|
||||
|
||||
source_refs = json.loads(person[12]) if isinstance(person[12], str) else person[12]
|
||||
self.assertIn(self.test_ref_filename, source_refs)
|
||||
|
||||
self.assertEqual(person[13], self.has_clothing)
|
||||
self.assertEqual(person[14], self.is_source)
|
||||
self.assertEqual(person[15], self.pose_description)
|
||||
self.assertEqual(person[16], self.pose_skeleton)
|
||||
|
||||
def test_05_reorder_group_updates_sort_orders(self):
|
||||
# Verify original order
|
||||
order_rows_before = database.get_group_order(self.group_id)
|
||||
filenames_before = [r[0] for r in order_rows_before]
|
||||
self.assertIn(self.test_ref_filename, filenames_before)
|
||||
self.assertIn(self.test_other_filename, filenames_before)
|
||||
|
||||
# Reverse the order of files and submit
|
||||
new_order = [self.test_other_filename, self.test_ref_filename]
|
||||
req_payload = {
|
||||
"filenames": new_order
|
||||
}
|
||||
|
||||
response = self.client.post(f"/groups/{self.group_id}/order", json=req_payload)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_data = response.json()
|
||||
self.assertEqual(res_data["group_id"], self.group_id)
|
||||
self.assertEqual(res_data["filenames"], new_order)
|
||||
|
||||
# Get order from database and verify state reflects the updates
|
||||
order_rows_after = database.get_group_order(self.group_id)
|
||||
filenames_after = [r[0] for r in order_rows_after]
|
||||
|
||||
# Verify custom sort orders are explicitly 0, 1
|
||||
self.assertEqual(filenames_after[:2], new_order)
|
||||
|
||||
person_other = database.get_person(self.test_other_filename)
|
||||
person_ref = database.get_person(self.test_ref_filename)
|
||||
|
||||
# Column 8 in database.get_person is sort_order
|
||||
self.assertEqual(person_other[8], 0)
|
||||
self.assertEqual(person_ref[8], 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user