• 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:
mike
2026-06-28 16:56:07 +02:00
parent da1fd4a26c
commit 3b9b9e829b
6 changed files with 881 additions and 239 deletions

View File

@@ -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); }
}

View File

@@ -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}

View File

@@ -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.

View 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()