Instructions to use MATLOWAI/MOSS-Image-SFT-DeBand with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusion Single File
How to use MATLOWAI/MOSS-Image-SFT-DeBand with Diffusion Single File:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Add DeBand Strong, research write-up, comparison viewer and tools
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +245 -0
- README.md +9 -0
- RESEARCH.md +162 -0
- loras/moss_image_sft_deband_strong_full_bf16.safetensors +3 -0
- research/README.md +57 -0
- research/anatomy_template.html +160 -0
- research/auto_score.py +33 -0
- research/bandviz.py +286 -0
- research/bench/drift_suite_v1.json +26 -0
- research/bench/prompts_v1.json +40 -0
- research/build_anatomy.py +124 -0
- research/build_compare.py +125 -0
- research/build_lowrank.py +34 -0
- research/build_trainset.py +57 -0
- research/compare_template.html +123 -0
- research/drift_runner.py +130 -0
- research/drift_score.py +100 -0
- research/edit_anatomy.py +69 -0
- research/edit_anatomy_c.py +41 -0
- research/export_gramsolve.py +56 -0
- research/gram_capture/flat_000_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_001_1216x832_00001_.png +0 -0
- research/gram_capture/flat_002_832x1216_00001_.png +0 -0
- research/gram_capture/flat_003_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_004_1216x832_00001_.png +0 -0
- research/gram_capture/flat_005_832x1216_00001_.png +0 -0
- research/gram_capture/flat_006_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_007_1216x832_00001_.png +0 -0
- research/gram_capture/flat_008_832x1216_00001_.png +0 -0
- research/gram_capture/flat_009_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_010_1216x832_00001_.png +0 -0
- research/gram_capture/flat_011_832x1216_00001_.png +0 -0
- research/gram_capture/flat_012_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_013_1216x832_00001_.png +0 -0
- research/gram_capture/flat_014_832x1216_00001_.png +0 -0
- research/gram_capture/flat_015_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_016_1216x832_00001_.png +0 -0
- research/gram_capture/flat_017_832x1216_00001_.png +0 -0
- research/gram_capture/flat_018_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_019_1216x832_00001_.png +0 -0
- research/gram_capture/flat_020_832x1216_00001_.png +0 -0
- research/gram_capture/flat_021_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_022_1216x832_00001_.png +0 -0
- research/gram_capture/flat_023_832x1216_00001_.png +0 -0
- research/gram_capture/flat_024_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_025_1216x832_00001_.png +0 -0
- research/gram_capture/flat_026_832x1216_00001_.png +0 -0
- research/gram_capture/flat_027_1024x1024_00001_.png +0 -0
- research/gram_capture/flat_028_1216x832_00001_.png +0 -0
- research/gram_capture/flat_029_832x1216_00001_.png +0 -0
.gitattributes
CHANGED
|
@@ -71,3 +71,248 @@ images/presets_product_glass_color.jpg filter=lfs diff=lfs merge=lfs -text
|
|
| 71 |
images/presets_product_glass_photo.jpg filter=lfs diff=lfs merge=lfs -text
|
| 72 |
images/widthsweep_fjord_sky.jpg filter=lfs diff=lfs merge=lfs -text
|
| 73 |
images/widthsweep_fog_forest.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
images/presets_product_glass_photo.jpg filter=lfs diff=lfs merge=lfs -text
|
| 72 |
images/widthsweep_fjord_sky.jpg filter=lfs diff=lfs merge=lfs -text
|
| 73 |
images/widthsweep_fog_forest.jpg filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
web/img/base/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
web/img/base/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
web/img/base/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
web/img/base/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
web/img/base/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
web/img/base/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
web/img/base/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
web/img/base/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
web/img/base/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
web/img/base/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
web/img/csolve_rho0.1_L21-41/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
web/img/csolve_rho0.1_L21-41/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 86 |
+
web/img/csolve_rho0.1_L21-41/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 87 |
+
web/img/csolve_rho0.1_L21-41/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 88 |
+
web/img/csolve_rho0.1_L21-41/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 89 |
+
web/img/csolve_rho0.1_L21-41/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 90 |
+
web/img/csolve_rho0.1_L21-41/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 91 |
+
web/img/csolve_rho0.1_L21-41/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 92 |
+
web/img/csolve_rho0.1_L21-41/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 93 |
+
web/img/dbA/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
web/img/dbA/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
web/img/dbA/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
web/img/dbA/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
web/img/dbA/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 98 |
+
web/img/dbA/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 99 |
+
web/img/dbA/solid_beige__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 100 |
+
web/img/dbA/solid_grey__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 101 |
+
web/img/dbA/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 102 |
+
web/img/dbA/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 103 |
+
web/img/dbA/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 104 |
+
web/img/deband_lora/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 105 |
+
web/img/deband_lora/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 106 |
+
web/img/deband_lora/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 107 |
+
web/img/deband_lora/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 108 |
+
web/img/deband_lora/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 109 |
+
web/img/deband_lora/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 110 |
+
web/img/deband_lora/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 111 |
+
web/img/deband_lora/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 112 |
+
web/img/deband_lora/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 113 |
+
web/img/deband_lora/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 114 |
+
web/img/deband_lora/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 115 |
+
web/img/deband_lora/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 116 |
+
web/img/dsolve_rho0.1_L21-41/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 117 |
+
web/img/dsolve_rho0.1_L21-41/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 118 |
+
web/img/dsolve_rho0.1_L21-41/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 119 |
+
web/img/dsolve_rho0.1_L21-41/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 120 |
+
web/img/dsolve_rho0.1_L21-41/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 121 |
+
web/img/dsolve_rho0.1_L21-41/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 122 |
+
web/img/dsolve_rho0.1_L21-41/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 123 |
+
web/img/dsolve_rho0.1_L21-41/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 124 |
+
web/img/dsolve_rho0.1_L21-41/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 125 |
+
web/img/dsolve_rho0.1_L21-41/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 126 |
+
web/img/dsolve_rho0.1_L21-41/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 127 |
+
web/img/dsolve_rho0.1_L21-41/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 128 |
+
web/img/gcsolve_mu0.3_rho0.05_cfg25/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 129 |
+
web/img/gcsolve_mu0.3_rho0.05_cfg25/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 130 |
+
web/img/gcsolve_mu0.3_rho0.05_cfg25/p_elder_man_west_african__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 131 |
+
web/img/gcsolve_mu0.3_rho0.05_cfg25/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 132 |
+
web/img/gcsolve_mu0.3_rho0.05_cfg25/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 133 |
+
web/img/gcsolve_mu0.3_rho0.05_cfg25/p_young_woman_east_asian__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 134 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 135 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 136 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 137 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 138 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 139 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 140 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 141 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 142 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 143 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 144 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 145 |
+
web/img/gcsolve_mu0.3_rho0.05_x1.2_L21-41/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 146 |
+
web/img/kijai_m05/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 147 |
+
web/img/kijai_m05/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 148 |
+
web/img/kijai_m05/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 149 |
+
web/img/kijai_m05/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 150 |
+
web/img/kijai_m05/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 151 |
+
web/img/kijai_m05/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 152 |
+
web/img/kijai_m05/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 153 |
+
web/img/kijai_m05/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 154 |
+
web/img/kijai_m05/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 155 |
+
web/img/kijai_m1/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 156 |
+
web/img/kijai_m1/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 157 |
+
web/img/kijai_m1/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 158 |
+
web/img/kijai_m1/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 159 |
+
web/img/kijai_m1/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 160 |
+
web/img/kijai_m1/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 161 |
+
web/img/kijai_m1/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 162 |
+
web/img/kijai_m1/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 163 |
+
web/img/kijai_m1/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 164 |
+
web/img/kijai_m1/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 165 |
+
web/img/kijai_m1/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 166 |
+
web/img/kijai_m1/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 167 |
+
web/img/nynxz_1.0/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 168 |
+
web/img/nynxz_1.0/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 169 |
+
web/img/nynxz_1.0/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 170 |
+
web/img/nynxz_1.0/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 171 |
+
web/img/nynxz_1.0/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 172 |
+
web/img/nynxz_1.0/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 173 |
+
web/img/nynxz_1.0/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 174 |
+
web/img/nynxz_1.0/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 175 |
+
web/img/nynxz_1.0/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 176 |
+
web/img/nynxz_1.0/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 177 |
+
web/img/nynxz_1.0/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 178 |
+
web/img/nynxz_1.0/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 179 |
+
web/img/nynxz_1.5/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 180 |
+
web/img/nynxz_1.5/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 181 |
+
web/img/nynxz_1.5/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 182 |
+
web/img/nynxz_1.5/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 183 |
+
web/img/nynxz_1.5/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 184 |
+
web/img/nynxz_1.5/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 185 |
+
web/img/nynxz_1.5/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 186 |
+
web/img/nynxz_1.5/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 187 |
+
web/img/nynxz_1.5/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 188 |
+
web/img/nynxz_1.5/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 189 |
+
web/img/nynxz_1.5/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 190 |
+
web/img/nynxz_1.5/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 191 |
+
web/img/nynxz_1.5_cfg25/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 192 |
+
web/img/nynxz_1.5_cfg25/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 193 |
+
web/img/nynxz_1.5_cfg25/p_elder_man_west_african__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 194 |
+
web/img/nynxz_1.5_cfg25/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 195 |
+
web/img/nynxz_1.5_cfg25/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 196 |
+
web/img/nynxz_1.5_cfg25/p_young_woman_east_asian__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 197 |
+
web/img/nynxz_1.5_gc03r005/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 198 |
+
web/img/nynxz_1.5_gc03r005/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 199 |
+
web/img/nynxz_1.5_gc03r005/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 200 |
+
web/img/nynxz_1.5_gc03r005/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 201 |
+
web/img/nynxz_1.5_gc03r005/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 202 |
+
web/img/nynxz_1.5_gc03r005/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 203 |
+
web/img/nynxz_1.5_gc03r005/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 204 |
+
web/img/nynxz_1.5_gc03r005/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 205 |
+
web/img/nynxz_1.5_gc03r005/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 206 |
+
web/img/nynxz_1.5_gc03r005/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 207 |
+
web/img/nynxz_1.5_gc03r005/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 208 |
+
web/img/nynxz_1.5_gc03r005/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 209 |
+
web/img/qualityplus_lora/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 210 |
+
web/img/qualityplus_lora/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 211 |
+
web/img/qualityplus_lora/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 212 |
+
web/img/qualityplus_lora/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 213 |
+
web/img/qualityplus_lora/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 214 |
+
web/img/qualityplus_lora/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 215 |
+
web/img/qualityplus_lora/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 216 |
+
web/img/qualityplus_lora/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 217 |
+
web/img/qualityplus_lora/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 218 |
+
web/img/qualityplus_lora/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 219 |
+
web/img/qualityplus_lora/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 220 |
+
web/img/qualityplus_lora/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 221 |
+
web/img/sft/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 222 |
+
web/img/sft/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 223 |
+
web/img/sft/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 224 |
+
web/img/sft/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 225 |
+
web/img/sft/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 226 |
+
web/img/sft/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 227 |
+
web/img/sft/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 228 |
+
web/img/sft/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 229 |
+
web/img/sft/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 230 |
+
web/img/sft/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 231 |
+
web/img/sft/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 232 |
+
web/img/sft/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 233 |
+
web/img/sft_cfg25/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 234 |
+
web/img/sft_cfg25/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 235 |
+
web/img/sft_cfg25/p_elder_man_west_african__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 236 |
+
web/img/sft_cfg25/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 237 |
+
web/img/sft_cfg25/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 238 |
+
web/img/sft_cfg25/p_young_woman_east_asian__s15.webp filter=lfs diff=lfs merge=lfs -text
|
| 239 |
+
web/img/solve_mu0_rho0.1_L21-41/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 240 |
+
web/img/solve_mu0_rho0.1_L21-41/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 241 |
+
web/img/solve_mu0_rho0.1_L21-41/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 242 |
+
web/img/solve_mu0_rho0.1_L21-41/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 243 |
+
web/img/solve_mu0_rho0.1_L21-41/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 244 |
+
web/img/solve_mu0_rho0.1_L21-41/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 245 |
+
web/img/solve_mu0_rho0.1_L21-41/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 246 |
+
web/img/solve_mu0_rho0.1_L21-41/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 247 |
+
web/img/solve_mu0_rho0.1_L21-41/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 248 |
+
web/img/solve_mu0_rho0.1_L21-41/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 249 |
+
web/img/solve_mu0_rho0.1_L21-41/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 250 |
+
web/img/strong_full/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 251 |
+
web/img/strong_full/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 252 |
+
web/img/strong_full/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 253 |
+
web/img/strong_full/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 254 |
+
web/img/strong_full/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 255 |
+
web/img/strong_full/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 256 |
+
web/img/strong_full/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 257 |
+
web/img/strong_full/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 258 |
+
web/img/strong_full/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 259 |
+
web/img/strong_full/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 260 |
+
web/img/strong_full/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 261 |
+
web/img/strong_full/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 262 |
+
web/img/strong_r128/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 263 |
+
web/img/strong_r128/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 264 |
+
web/img/strong_r128/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 265 |
+
web/img/strong_r128/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 266 |
+
web/img/strong_r128/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 267 |
+
web/img/strong_r128/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 268 |
+
web/img/strong_r128/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 269 |
+
web/img/strong_r128/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 270 |
+
web/img/strong_r128/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 271 |
+
web/img/strong_r128/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 272 |
+
web/img/strong_r128/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 273 |
+
web/img/strong_r128/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 274 |
+
web/img/strong_r256/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 275 |
+
web/img/strong_r256/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 276 |
+
web/img/strong_r256/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 277 |
+
web/img/strong_r256/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 278 |
+
web/img/strong_r256/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 279 |
+
web/img/strong_r256/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 280 |
+
web/img/strong_r256/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 281 |
+
web/img/strong_r256/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 282 |
+
web/img/strong_r256/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 283 |
+
web/img/strong_r256/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 284 |
+
web/img/strong_r256/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 285 |
+
web/img/strong_r256/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 286 |
+
web/img/strong_r512/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 287 |
+
web/img/strong_r512/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 288 |
+
web/img/strong_r512/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 289 |
+
web/img/strong_r512/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 290 |
+
web/img/strong_r512/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 291 |
+
web/img/strong_r512/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 292 |
+
web/img/strong_r512/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 293 |
+
web/img/strong_r512/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 294 |
+
web/img/strong_r512/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 295 |
+
web/img/strong_r512/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 296 |
+
web/img/strong_r512/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 297 |
+
web/img/strong_r512/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 298 |
+
web/img/strong_r64/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 299 |
+
web/img/strong_r64/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 300 |
+
web/img/strong_r64/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 301 |
+
web/img/strong_r64/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 302 |
+
web/img/strong_r64/p_child_boy_black__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 303 |
+
web/img/strong_r64/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 304 |
+
web/img/strong_r64/p_elder_man_west_african__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 305 |
+
web/img/strong_r64/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 306 |
+
web/img/strong_r64/p_young_woman_east_asian__s14.webp filter=lfs diff=lfs merge=lfs -text
|
| 307 |
+
web/img/strong_r64/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 308 |
+
web/img/strong_r64/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 309 |
+
web/img/strong_r64/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 310 |
+
web/img/swap055_L21-41/a_anime_girl_school__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 311 |
+
web/img/swap055_L21-41/bench_fog_forest__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 312 |
+
web/img/swap055_L21-41/o_kitchen_scene__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 313 |
+
web/img/swap055_L21-41/p_child_boy_black__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 314 |
+
web/img/swap055_L21-41/p_elder_man_west_african__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 315 |
+
web/img/swap055_L21-41/p_young_woman_east_asian__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 316 |
+
web/img/swap055_L21-41/t_cafe_chalkboard__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 317 |
+
web/img/swap055_L21-41/x_golden_retriever__s11.webp filter=lfs diff=lfs merge=lfs -text
|
| 318 |
+
web/img/swap055_L21-41/x_red_fox_snow__s11.webp filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -15,6 +15,8 @@ tags:
|
|
| 15 |
|
| 16 |
The MOSS-Image **SFT** model often draws faint, evenly spaced **vertical color stripes** across smooth areas like skies, fog, backdrops and walls. This repo has two **banding-fix LoRAs** that remove them while keeping the image close to what SFT would have made, plus **preset workflows** that pair the fix with community speed LoRAs for 10–14 step (or 4–8 step) generation.
|
| 17 |
|
|
|
|
|
|
|
| 18 |
## TL;DR
|
| 19 |
|
| 20 |
**Just want one file that works?** [MOSS-Image-SFT-DeBand-int8](https://huggingface.co/MATLOWAI/MOSS-Image-SFT-DeBand-int8) has the fix (and the Fast preset) merged into ready-to-run 5.4 GB int8 models. No LoRAs or custom nodes.
|
|
@@ -25,6 +27,8 @@ The MOSS-Image **SFT** model often draws faint, evenly spaced **vertical color s
|
|
| 25 |
4. **Or** drag [`workflows/moss_banding_fix_presets.json`](workflows/moss_banding_fix_presets.json) into ComfyUI and pick a preset: **Quality** (25 steps), **Quality+** (both fixes), **Balanced** (14 steps), **Fast** / **Fast alt** (10 steps). The files each preset needs are listed [below](#preset-workflows-in-detail).
|
| 26 |
5. Want 4–8 steps? [`workflows/moss_banding_fix_presets_rapid_moss.json`](workflows/moss_banding_fix_presets_rapid_moss.json) adds the **Turbo** presets. It needs two custom nodes; see the setup below. Keep Turbo near 1 MP (e.g. 1024×1024); bigger sizes tile.
|
| 27 |
|
|
|
|
|
|
|
| 28 |
How it works: each LoRA is an exact weight difference that turns some of SFT's transformer layers back into the Base model's weights. The main fix covers the **last five layers (37–41)**, the extra fix covers **32–36**, and everything else is untouched. How those layers were found is [further down](#how-the-layers-were-found), with the full write-up in [ANALYSIS.md](ANALYSIS.md).
|
| 29 |
|
| 30 |
## Examples
|
|
@@ -272,6 +276,10 @@ The full write-up, with the ablation charts, probe figures, step-window tests, o
|
|
| 272 |
| `moss_image_sft_banding_fix_L37-41_bf16_splitqkv.safetensors` | 1.13 GB | The main fix with q/k/v split, for the split model (rapid-moss workflow) |
|
| 273 |
| `split_qkv/moss_image_sft_bf16_split_qkv.safetensors` | 10.2 GB | The official MOSS-Image-SFT transformer as one file with q/k/v separate (rapid-moss workflow only) |
|
| 274 |
| `workflows/*.json` | | The two preset workflows; `*_api.json` are API-format copies for scripting |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
SHA-256:
|
| 277 |
- main fix `01329abd340dd9cf55677381a11558fe5307c7fd648f1988b441172fbbd86fd5`
|
|
@@ -283,4 +291,5 @@ SHA-256:
|
|
| 283 |
|
| 284 |
- [OpenMOSS](https://huggingface.co/OpenMOSS-Team) for MOSS-Image Base and SFT (Apache-2.0), and [Comfy-Org](https://huggingface.co/Comfy-Org/MOSS-Image) for the ComfyUI repack.
|
| 285 |
- [lodestones](https://huggingface.co/lodestones/MOSS-Image-SFT-8-steps-dmd) and [darask0](https://huggingface.co/darask0/MOSS-Image_4-step_adapter_lora) for the 8-step DMD and 4-step adapters used in the speed and Turbo presets.
|
|
|
|
| 286 |
- [Kijai](https://huggingface.co/Kijai/MOSS-image_experimental) for the SFT-difference LoRAs, and a big thanks for pointing me in the right direction: Kijai's note that the SFT causes the banding in the later blocks, fixable with negative strength, is where this investigation started.
|
|
|
|
| 15 |
|
| 16 |
The MOSS-Image **SFT** model often draws faint, evenly spaced **vertical color stripes** across smooth areas like skies, fog, backdrops and walls. This repo has two **banding-fix LoRAs** that remove them while keeping the image close to what SFT would have made, plus **preset workflows** that pair the fix with community speed LoRAs for 10–14 step (or 4–8 step) generation.
|
| 17 |
|
| 18 |
+
> **Update, October 2026.** Since this repo went up, [nynxz's Destripe LoRA](https://huggingface.co/nynxz/MOSS-Image) (built on this investigation) has become the gentlest fix: it reduces the stripes while keeping images almost exactly as SFT made them. This update adds **DeBand Strong** (experimental: the strongest cleanup short of Base, with more drift), an **interactive comparison of every fix** ([`web/`](web/index.html)), the **full research write-up** ([RESEARCH.md](RESEARCH.md)) including what didn't work, and the **measurement tools** ([`research/`](research/README.md)). Quick guide: minimal change → nynxz Destripe; cleanest flat backgrounds → DeBand Strong; one-file int8 → the int8 repo below.
|
| 19 |
+
|
| 20 |
## TL;DR
|
| 21 |
|
| 22 |
**Just want one file that works?** [MOSS-Image-SFT-DeBand-int8](https://huggingface.co/MATLOWAI/MOSS-Image-SFT-DeBand-int8) has the fix (and the Fast preset) merged into ready-to-run 5.4 GB int8 models. No LoRAs or custom nodes.
|
|
|
|
| 27 |
4. **Or** drag [`workflows/moss_banding_fix_presets.json`](workflows/moss_banding_fix_presets.json) into ComfyUI and pick a preset: **Quality** (25 steps), **Quality+** (both fixes), **Balanced** (14 steps), **Fast** / **Fast alt** (10 steps). The files each preset needs are listed [below](#preset-workflows-in-detail).
|
| 28 |
5. Want 4–8 steps? [`workflows/moss_banding_fix_presets_rapid_moss.json`](workflows/moss_banding_fix_presets_rapid_moss.json) adds the **Turbo** presets. It needs two custom nodes; see the setup below. Keep Turbo near 1 MP (e.g. 1024×1024); bigger sizes tile.
|
| 29 |
|
| 30 |
+
6. **Cleanest flat areas (experimental):** [`loras/moss_image_sft_deband_strong_full_bf16.safetensors`](https://huggingface.co/MATLOWAI/MOSS-Image-SFT-DeBand/resolve/main/loras/moss_image_sft_deband_strong_full_bf16.safetensors) at **1.0** (or **1.2** for the most reliable cleanup), on its own (not stacked with the fixes above). It removes the most stripe of anything except Base, but faces and fine detail move more, and it is tuned to this repo's preset sampler (CFG 3 + SLG + TSR). Details in [RESEARCH.md](RESEARCH.md#1-which-fix-to-use).
|
| 31 |
+
|
| 32 |
How it works: each LoRA is an exact weight difference that turns some of SFT's transformer layers back into the Base model's weights. The main fix covers the **last five layers (37–41)**, the extra fix covers **32–36**, and everything else is untouched. How those layers were found is [further down](#how-the-layers-were-found), with the full write-up in [ANALYSIS.md](ANALYSIS.md).
|
| 33 |
|
| 34 |
## Examples
|
|
|
|
| 276 |
| `moss_image_sft_banding_fix_L37-41_bf16_splitqkv.safetensors` | 1.13 GB | The main fix with q/k/v split, for the split model (rapid-moss workflow) |
|
| 277 |
| `split_qkv/moss_image_sft_bf16_split_qkv.safetensors` | 10.2 GB | The official MOSS-Image-SFT transformer as one file with q/k/v separate (rapid-moss workflow only) |
|
| 278 |
| `workflows/*.json` | | The two preset workflows; `*_api.json` are API-format copies for scripting |
|
| 279 |
+
| `loras/moss_image_sft_deband_strong_full_bf16.safetensors` | 4.76 GB | DeBand Strong (experimental): closed-form Gram solve over layers 21–41 (μ=0.3, ρ=0.05) (full rank; compressed versions lose the fix, see RESEARCH.md) |
|
| 280 |
+
| `RESEARCH.md` | | Full write-up: the artifact, all approaches, benchmark, results, negative results |
|
| 281 |
+
| `web/` | | Static comparison pages (open `web/index.html`): every fix side by side with colour view and leaderboard; per-layer edit anatomy; learnings |
|
| 282 |
+
| `research/` | | Measurement and solve tools, benchmark prompts, the lab node, and every score row |
|
| 283 |
|
| 284 |
SHA-256:
|
| 285 |
- main fix `01329abd340dd9cf55677381a11558fe5307c7fd648f1988b441172fbbd86fd5`
|
|
|
|
| 291 |
|
| 292 |
- [OpenMOSS](https://huggingface.co/OpenMOSS-Team) for MOSS-Image Base and SFT (Apache-2.0), and [Comfy-Org](https://huggingface.co/Comfy-Org/MOSS-Image) for the ComfyUI repack.
|
| 293 |
- [lodestones](https://huggingface.co/lodestones/MOSS-Image-SFT-8-steps-dmd) and [darask0](https://huggingface.co/darask0/MOSS-Image_4-step_adapter_lora) for the 8-step DMD and 4-step adapters used in the speed and Turbo presets.
|
| 294 |
+
- [nynxz](https://huggingface.co/nynxz/MOSS-Image) for the gradient-attributed Destripe LoRA and its write-up; it is the comparison point for everything in RESEARCH.md.
|
| 295 |
- [Kijai](https://huggingface.co/Kijai/MOSS-image_experimental) for the SFT-difference LoRAs, and a big thanks for pointing me in the right direction: Kijai's note that the SFT causes the banding in the later blocks, fixable with negative strength, is where this investigation started.
|
RESEARCH.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MOSS-Image SFT vertical stripes: research notes (October 2026)
|
| 2 |
+
|
| 3 |
+
This is the full write-up behind the DeBand LoRAs: what the stripe artifact is, every fix we and others tried, how they were measured, and what didn't work. It is written to be read by people and by the LLM helping them, so it states facts, numbers and file locations plainly. The interactive comparison of all fixes is in [`web/index.html`](web/index.html) (download the `web/` folder and open it locally, or use the hosted copy if linked from the README).
|
| 4 |
+
|
| 5 |
+
## 1. Which fix to use
|
| 6 |
+
|
| 7 |
+
| You want | Use | Trade-off |
|
| 8 |
+
|---|---|---|
|
| 9 |
+
| Stripes reduced, everything else as SFT made it | [nynxz's Destripe LoRA](https://huggingface.co/nynxz/MOSS-Image) @1.0 (768 px) / 1.5 (1024 px) | Gentlest fix measured (faces 0.94–0.96 similar to SFT). Only partly cleans flat colour areas (about −4 dB; stripes fainter but still visible in the colour view). |
|
| 10 |
+
| Clean flat backgrounds, skies, studio backdrops | **DeBand Strong** (`loras/`, this repo, experimental) at 1.0, or 1.2 for the most reliable cleanup | Strongest cleanup short of the Base model (at 1.2: typical −10.7 dB, worst −5.5 dB on solids; faces 0.77). Faces and fine detail move more (0.83 similarity), and it is tuned to the CFG 3 + SLG + TSR preset sampler; at CFG 2.5 without SLG/TSR it drifts much more. |
|
| 11 |
+
| The original one-file fix | DeBand main fix (layers 37–41) / Quality+ (32–41) | Exact Base weights in the last layers. Helps most at 768 px and on the vertical stripe score; on 1024 px solids the orientation-free measure shows little net change (main) or about −3 dB (Quality+). |
|
| 12 |
+
| No stripes at all | MOSS-Image Base | Different model: faces and style change a lot (0.45 similarity). |
|
| 13 |
+
|
| 14 |
+
Stacking nynxz's LoRA on top of DeBand Strong adds nothing over Strong alone.
|
| 15 |
+
|
| 16 |
+
## 2. The artifact
|
| 17 |
+
|
| 18 |
+
- **What it is:** faint, evenly spaced vertical colour stripes in smooth areas (skies, fog, walls, backdrops). A chroma shift with almost no brightness change; invisible until colour is boosted, then obvious.
|
| 19 |
+
- **Count, not size:** about 45–65 cycles across the image width (≈48 at 1024 px) at every tested resolution and aspect ratio (1–2 MP; 1:1, 4:3, 3:4, 16:9, 9:16). Always vertical.
|
| 20 |
+
- **Tied to position encoding:** rescaling only the horizontal RoPE positions moves the count (column scale 0.8 → ≈64 cycles, 1.25 → ≈40); a common shift of all positions does nothing; shifting the image relative to the text halves it. Not a VAE or fixed pixel period.
|
| 21 |
+
- **Mostly a damping failure:** the ER-SDE sampler re-injects noise every step and SFT fails to clean up that band of it. Lowering ER-SDE `s_noise` nearly removes the stripes but fades and blurs images (0.7: contrast loss; 0.5: text disappears; 0: blank) — a diagnostic, not a fix. CFG scales the stripes (CFG 1 ≪ CFG 3 ≪ CFG 5); SLG off helps somewhat; TSR is irrelevant; Euler roughly halves them.
|
| 22 |
+
- **Where in the model:** layers 21–41 (of 42). Reverting 37–41 to Base fixes most cases at 768 px; 32–36 matters at larger sizes. Gradient attribution (nynxz) puts the causal directions in layers 24–41, mostly the MLP `ffn_up_gate`.
|
| 23 |
+
- **When:** the stripe forms mid-sampling (roughly steps 5–14 of 25) and is visible in the model's x0 prediction.
|
| 24 |
+
- **Not the VAE:** it appears with every VAE tried and only with SFT, never with Base (confirmed independently in the [OpenMOSS SFT stripe thread](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT/discussions/2)).
|
| 25 |
+
- **Where it probably comes from (unverified hypothesis):** community members suggest the SFT stage was trained partly on outputs of GPT Image 2/2.5, whose images carry a similar faint vertical colour-stripe pattern (described there as a watermark-like artifact). That fits what we measured: a pattern learned from training images would be fixed relative to the image width (a fixed cycle count, tied to the horizontal position encoding), would appear only in the fine-tuned SFT and not in Base, and would be a narrow, separable behaviour (which is why 64 directions can remove it). Testing it would mean running the same colour-view and spectrum measurement on GPT Image outputs; we have not done that.
|
| 26 |
+
|
| 27 |
+
## 3. Approaches compared
|
| 28 |
+
|
| 29 |
+
Every approach adds back some piece **D** of **Δ = W_Base − W_SFT** (what fine-tuning changed). They differ in how D is chosen.
|
| 30 |
+
|
| 31 |
+
1. **Block revert (DeBand v1, published):** D = Δ in chosen layers. Picks by location only; undoes everything SFT learned there.
|
| 32 |
+
2. **Compressed revert (Kijai's rank-512 LoRA at −1 / −0.5):** a low-rank approximation of Δ over layers 3, 4, 21–41. Measured as non-selective: it changes layer outputs on solid-colour inputs (83 % of a full revert) about as much as on general inputs (79 %).
|
| 33 |
+
3. **Closed-form Gram solve (DeBand Strong, new):** per linear, the smallest weight change that makes the layer output what Base outputs on the inputs it sees while SFT draws solid colours, while keeping SFT's output on the horizontal detail of general prompts:
|
| 34 |
+
`D = Δ · G_flat · (G_flat + μ·G_detail + ε·I)⁻¹`. Picks by input statistics. Selective (92 % output change on solid-colour detail vs 75 % on general detail at ρ=0.1), but undoes everything those inputs touch, and it is fitted to the sampler the statistics were recorded under.
|
| 35 |
+
4. **Gradient-picked directions (nynxz Destripe):** SVD of each layer's Δ into rank-1 directions; one backward pass scores every direction by its contribution to the 48-cycle stripe energy of the x0 prediction over steps 4–13; the 64 strongest are subtracted (25 matrices, rank 1–7, 1.9 MB). Picks by measured effect. Same directions at negative strength raise stripe energy ≈22× (their sign check).
|
| 36 |
+
5. **Training (negative):** a rank-32 LoRA trained on clean solid renders plus SFT's own general renders. No effect at 200 steps; at 600 steps it learned "remove fine detail" and degraded everything (blur, colour shifts, faces 0.12 similar).
|
| 37 |
+
|
| 38 |
+
### 3a. How nynxz's LoRA is so small and still works
|
| 39 |
+
|
| 40 |
+
The numbers, measured from their file and our weight analysis:
|
| 41 |
+
|
| 42 |
+
| | nynxz Destripe | DeBand Strong (Gram solve) | DeBand v1 main fix |
|
| 43 |
+
|---|---|---|---|
|
| 44 |
+
| Matrices touched | 25 of 252 | 84 (all of layers 21–41) | 20 (layers 37–41) |
|
| 45 |
+
| Stored numbers | 945,152 (1.9 MB) | ≈2.4 billion (4.8 GB) | ≈570 million (1.1 GB) |
|
| 46 |
+
| Share of SFT's weight change undone (energy) | **0.09 %** of all of Δ (0.2 % of layers 24–41) | ≈30 % of layers 21–41 | 100 % of layers 37–41 |
|
| 47 |
+
|
| 48 |
+
How the recipe gets there (from their model card, their discussion post and the metadata embedded in the LoRA: K = 256, 768 px, seeds 11 and 14, steps 4–13, ER-SDE, CFG 2.5):
|
| 49 |
+
|
| 50 |
+
1. **Break the change into pieces.** Any matrix change Δ is a sum of rank-1 pieces, Δ = Σⱼ σⱼ·uⱼ·vⱼᵀ (an SVD). Each piece is one "input pattern vⱼ → output pattern uⱼ" rule that fine-tuning added. Storing one piece costs only (rows + columns) numbers, e.g. 19,456 for an MLP up/gate matrix instead of 50 million for the full matrix.
|
| 51 |
+
2. **Score every piece with one backward pass.** Make the stripe a single number: the energy of the 48-cycle column pattern in the model's predicted clean image, summed over steps 4–13 of real sampling runs. Backpropagate it once to get the gradient G = ∂E/∂W for every weight matrix. The effect of undoing piece j is then, to first order, just a dot product: score ≈ σⱼ·uⱼᵀ·G·vⱼ. With the gradients in hand, scoring all 256 pieces of all 252 matrices costs a few matrix-vector products. No per-piece renders and no search.
|
| 52 |
+
3. **Remove only the top 64.** Subtract the 64 pieces that feed the stripe most. They land in 25 matrices (layers 24–41, mostly the MLP's `ffn_up_gate`), 1–7 pieces each. 48 of the 64 are among the 8 largest pieces of their matrix, but most of each matrix's large pieces are not picked, and 227 matrices are left alone entirely.
|
| 53 |
+
4. **Check it's causal.** Adding the same pieces back with the opposite sign makes the stripes ≈22× stronger. A correlation wouldn't do that.
|
| 54 |
+
|
| 55 |
+
**Why it's so gentle:** the stripe is one narrow behaviour (one frequency, colour only, one part of sampling), so a handful of pieces carry most of it. Everything else SFT learned (faces, style, prompt following) lives in the other 99.9 % of the change, which the LoRA never touches. Choosing by location (whole layers) or by input statistics (the Gram solve) can't be that precise, because those criteria also grab pieces that matter for everything else.
|
| 56 |
+
|
| 57 |
+
**Why it's only a partial fix on our screen (≈ −4 dB on 1024 px solids, vs ≈ −16 for Base):** the score is first-order (it predicts the effect of tiny changes; removing a whole piece is a large change); it was computed at 768 px with CFG 2.5 on 4 prompts and 2 seeds; it targets the vertical 48-cycle energy, so residue at other angles or frequencies isn't counted; and it stops at 64 pieces. Each of these is a knob: more prompts and seeds, the sampler settings people actually use, an orientation-free objective, an iterative "remove, re-score, repeat" loop, and a per-piece strength instead of all-or-nothing.
|
| 58 |
+
|
| 59 |
+
**A related observation from our side:** compressing DeBand Strong into a low-rank LoRA (keeping its biggest SVD pieces) keeps its drift but loses most of its cleanup, at every size tried:
|
| 60 |
+
|
| 61 |
+
| DeBand Strong size | Solids median dB | mean | worst | ArcFace |
|
| 62 |
+
|---|---|---|---|---|
|
| 63 |
+
| full rank (4.8 GB) | −6.9 | −11.2 | −2.0 | 0.83 |
|
| 64 |
+
| rank 512 (1.1 GB) | −4.5 | −4.9 | −0.2 | 0.84 |
|
| 65 |
+
| rank 256 (530 MB) | −4.3 | −4.8 | +0.6 | 0.83 |
|
| 66 |
+
| rank 128 (264 MB) | −2.9 | −3.3 | +0.9 | 0.86 |
|
| 67 |
+
| rank 64 (132 MB) | −3.1 | −3.6 | +1.0 | 0.89 |
|
| 68 |
+
| nynxz Destripe @1.5 (1.9 MB), for reference | −3.9 | −4.4 | −1.8 | 0.94 |
|
| 69 |
+
|
| 70 |
+
Ranking pieces by size is not ranking them by effect; that's the gap nynxz's gradient scoring closes. It's also why DeBand Strong ships at full rank or not at all.
|
| 71 |
+
|
| 72 |
+
**The lesson that transfers:** choose what to undo by the behaviour you want to change, measured through the real computation. Location (1) and input statistics (3) are proxies; gradients through sampling (4) measure the target directly and give the smallest edits.
|
| 73 |
+
|
| 74 |
+
## 4. How everything was measured
|
| 75 |
+
|
| 76 |
+
Benchmark ("screen"): 13 items through the stock preset workflow (ER-SDE, 25 steps, CFG 3, SLG layers 22/24 over 10–70 %, TSR 0.95): solid grey/blue/beige at 1024², two banding-bench scenes at 768×1024, three portraits, one anime, two animals, a chalkboard, an interior; seed 11, plus seed 14 (solids + portraits) and seed 15 (solids) for the main arms. All numbers are relative to plain SFT with the same prompt and seed.
|
| 77 |
+
|
| 78 |
+
- **Solids, HF dB (primary):** orientation-free colour-detail power at periods under 40 px (centre 512² crop of the R−G and B−(R+G)/2 channels) relative to SFT, in dB; lower = cleaner. Catches stripes rotated into diagonals.
|
| 79 |
+
- **ArcFace:** identity similarity of the largest face to SFT's face for the same prompt/seed (1.0 = same person).
|
| 80 |
+
- **DINO distance / SSIM:** overall drift on non-solid items.
|
| 81 |
+
- **Vertical score (bandviz `local_score`):** 48-cycle vertical fingerprint, ≈1.0 clean. Useful but fooled by rotated residue and real vertical patterns (pleats, sails).
|
| 82 |
+
|
| 83 |
+
Single solid images swing 10–30 dB between seeds because a solid tends to come out either fully clean or still striped. Report median, mean and worst case together; treat differences under ~3 dB (or ~0.03 ArcFace) as noise.
|
| 84 |
+
|
| 85 |
+
Results (screen; solids = grey/blue/beige × seeds 11/14/15, faces = 3 portraits × seeds 11/14, relative to plain SFT):
|
| 86 |
+
|
| 87 |
+
| Fix | Solids median dB | mean | worst | ArcFace mean | min | DINO dist | SSIM | solids n |
|
| 88 |
+
|---|---|---|---|---|---|---|---|---|
|
| 89 |
+
| MOSS-Image Base (reference) | −16.6 | −28.6 | −10.4 | 0.45 | 0.28 | 0.150 | 0.58 | 9 |
|
| 90 |
+
| DeBand v1 main fix (37–41) | +0.4 | −0.0 | +2.3 | 0.91 | 0.72 | 0.042 | 0.86 | 9 |
|
| 91 |
+
| DeBand v1 Quality+ (32–41) | −3.0 | −3.2 | +1.3 | 0.85 | 0.67 | 0.067 | 0.73 | 9 |
|
| 92 |
+
| Kijai rank-512 LoRA @ −0.5 | −1.5 | −1.7 | +1.1 | 0.85 | 0.70 | 0.056 | 0.77 | 6 |
|
| 93 |
+
| Kijai rank-512 LoRA @ −1 | −5.8 | −6.0 | −1.3 | 0.76 | 0.59 | 0.080 | 0.69 | 9 |
|
| 94 |
+
| nynxz Destripe @1.0 | −3.2 | −3.5 | −1.9 | 0.94 | 0.78 | 0.028 | 0.93 | 9 |
|
| 95 |
+
| nynxz Destripe @1.5 | −3.9 | −4.4 | −1.8 | 0.94 | 0.71 | 0.031 | 0.92 | 9 |
|
| 96 |
+
| Plain 55 % blend toward Base, 21–41 (control) | −4.9 | −5.6 | −4.7 | 0.77 | 0.64 | 0.074 | 0.68 | 3 |
|
| 97 |
+
| Gram solve ρ=0.1, no detail protection | −6.6 | −13.6 | −3.8 | 0.72 | 0.50 | 0.091 | 0.66 | 9 |
|
| 98 |
+
| **DeBand Strong** (Gram solve μ=0.3 ρ=0.05, full rank) | −6.9 | −11.2 | −2.0 | 0.83 | 0.66 | 0.065 | 0.71 | 9 |
|
| 99 |
+
| **DeBand Strong at strength 1.2** | −10.7 | −13.5 | −5.5 | 0.77 | 0.66 | 0.078 | 0.67 | 9 |
|
| 100 |
+
| Gram solve μ=0.3 ρ=0.1 at strength 1.2 | −10.0 | −10.7 | −5.1 | 0.80 | 0.67 | 0.077 | 0.68 | 9 |
|
| 101 |
+
| nynxz @1.5 stacked on Strong's solve | −6.9 | −7.1 | −3.1 | 0.82 | 0.65 | 0.071 | 0.71 | 9 |
|
| 102 |
+
| Trained de-band LoRA, 600 steps (failed) | −3.1 | −2.6 | +5.2 | 0.12 | 0.03 | 0.363 | 0.56 | 9 |
|
| 103 |
+
|
| 104 |
+
At nynxz's sampler settings (CFG 2.5, no SLG/TSR; relative to SFT at the same settings):
|
| 105 |
+
|
| 106 |
+
| Fix | Solids median dB | mean | worst | ArcFace mean |
|
| 107 |
+
|---|---|---|---|---|
|
| 108 |
+
| nynxz @1.5 | −4.6 | −4.6 | −3.0 | 0.96 |
|
| 109 |
+
| DeBand Strong's solve | −5.6 | −7.6 | −2.6 | 0.62 |
|
| 110 |
+
|
| 111 |
+
## 5. Findings worth knowing
|
| 112 |
+
|
| 113 |
+
**Measuring**
|
| 114 |
+
- Vertical-only stripe scores are fooled: several solves rotated the stripe into short diagonals and scored "clean". Use an orientation-free measure on solids and look at the colour view.
|
| 115 |
+
- Means get dragged by the occasional perfectly clean solid (one grey reached −102 dB); medians and worst cases tell the typical story.
|
| 116 |
+
- In every layer about half of the input energy is one direction: each token's overall colour/brightness plus the timestep modulation shift. Any edit touching it looks "big" by raw output-change energy (ρ=10 changes ~1 % of the weights but shows 79 % "output change"). Measure selectivity on row-centred activations.
|
| 117 |
+
|
| 118 |
+
**The Gram solve**
|
| 119 |
+
- Genuinely selective: a plain 55 % blend toward Base over the same layers (a similar-size edit) reaches only −5.6 dB at the same face drift.
|
| 120 |
+
- Protect detail, not everything: preserving general prompts' full statistics (μ on the raw general Gram) kills the fix, because the colour/brightness direction is shared. Preserving only their horizontal detail (μ=0.3 on the row-centred Gram) keeps most of the cleanup and lifts face similarity from 0.72 to 0.83.
|
| 121 |
+
- Matching Base only on horizontal detail does nothing for stripes (≈0 dB) while still moving faces. Matching Base only on the remainder (per-row colour/brightness plus text tokens) gives ≈ −5 dB with faces at 0.86. With a shared denominator the two edits add exactly to the full one; the extra image-level effect of using both comes from nonlinear sampling.
|
| 122 |
+
- It needs all of layers 21–41 (either half alone: −3 to −5 dB) and the early/middle steps (applying it only in steps 13–25: ≈ −1 dB).
|
| 123 |
+
- It is sampler-specific: fitted under CFG 3 + SLG + TSR, it keeps faces at 0.83 there but 0.62 at CFG 2.5 without SLG/TSR. nynxz's directions keep 0.94–0.96 on both.
|
| 124 |
+
|
| 125 |
+
**Other**
|
| 126 |
+
- Per-layer weight-change maps and selectivity charts for every fix are in `web/edit_anatomy.html`.
|
| 127 |
+
- Training MOSS-Image in ComfyUI's `TrainLoraNode` works after a one-line dtype fix in the training path (q/k cast back after norm + RoPE), with the memory compiler off and `checkpoint_depth=2` (13.6 GB at 1024², 1.26 s/step on an RTX 5090). Depth 1 runs out of memory at ~27.5 GB.
|
| 128 |
+
|
| 129 |
+
## 6. Beyond MOSS: the same lesson on a character LoRA
|
| 130 |
+
|
| 131 |
+
The same question — keep a concept when asked for, change nothing else — appears with character LoRAs ([DeTangle](https://matlowai.github.io/DeTangle/)). Its "A-side surgery" refits one LoRA factor from captured activations, the same family as the Gram solve, and shows the same weakness. On two of eight test scenes (a sci-fi "space station" and a fantasy "wizard workshop", the only prompts ending in "Photorealistic live-action science-fiction/fantasy film still.") the character's likeness collapses after surgery and the face falls back to the base model's own default for the scene; for the space station that default resembles the model's own rendering of Sigourney Weaver (face match 0.50; the model's named-actress renders match each other at 0.9).
|
| 132 |
+
|
| 133 |
+
Probe results (424 renders, same protocol as the DeTangle study):
|
| 134 |
+
- Only transformer blocks 8–15 matter: restoring the original LoRA there brings likeness back (0.36–0.55); every other block window, including the first, does nothing.
|
| 135 |
+
- Those same blocks bring the unwanted bleed back (+0.19 to +0.30): the parts that let the character win against a strong scene prior are the parts that leak it into untriggered prompts. Input statistics cannot separate them.
|
| 136 |
+
- The film-still sentence is a real but partial cause: adding it to other scenes cuts post-surgery likeness by 0.15–0.26 while the original LoRA is unaffected; stripping all cinematic phrasing recovers most of it.
|
| 137 |
+
- After surgery the trigger loses to any explicitly described identity (a named actress or an ethnicity descriptor).
|
| 138 |
+
|
| 139 |
+
The proposed next step is nynxz's method applied there: score the removed LoRA directions in blocks 8–15 by their effect on likeness and bleed, per scene, and keep only what helps without leaking. (The character LoRA itself is private and not published.)
|
| 140 |
+
|
| 141 |
+
## 7. Lineage and credits
|
| 142 |
+
|
| 143 |
+
- **Kijai** observed that SFT's later blocks cause the banding and that a negative SFT-difference LoRA reduces it — the starting point.
|
| 144 |
+
- **DeBand v1 (MATLOWAI):** block-swap ablations located layers 37–41 / 32–36; `bandviz` (48-cycle fingerprint and colour view); exact-revert LoRAs and preset workflows.
|
| 145 |
+
- **nynxz:** took the 48-cycle stripe signature, the step window and the layer range from DeBand, replaced "revert whole layers" with gradient-scored SVD directions, and measured with `bandviz` on 12 held-out images. Their write-up: [model card](https://huggingface.co/nynxz/MOSS-Image), [discussion with OpenMOSS](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT/discussions/8).
|
| 146 |
+
- **Community threads:** the [OpenMOSS SFT stripe thread](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT/discussions/2) (first reports with videos, the VAE debunk, the GPT Image hypothesis) and nynxz's [write-up for the MOSS team](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT/discussions/8), which asks whether SFT used position-scaling, bucketing or resized/synthetic data with this pattern, and offers per-direction scores. No reply from OpenMOSS as of 2026-10-19.
|
| 147 |
+
- **This update:** the orientation-free benchmark, Gram solves (DeBand Strong), edit anatomy, negative results, and the head-to-head including nynxz's LoRA on matched settings.
|
| 148 |
+
|
| 149 |
+
## 8. Open questions
|
| 150 |
+
|
| 151 |
+
- nynxz-style gradient scoring with an orientation-free objective, scored across several sampler settings — likely the best of both (selective and complete).
|
| 152 |
+
- Why SFT damps that band less than Base: a replay harness that feeds the same noisy latents to both and measures per-step gain at the stripe frequency is designed but not built.
|
| 153 |
+
- Whether a fixed SFT checkpoint is possible upstream (see nynxz's discussion with OpenMOSS).
|
| 154 |
+
- Whether GPT Image outputs carry the same 48-cycle colour pattern (the origin hypothesis above).
|
| 155 |
+
|
| 156 |
+
## 9. Files in this update
|
| 157 |
+
|
| 158 |
+
| Path | What |
|
| 159 |
+
|---|---|
|
| 160 |
+
| `loras/moss_image_sft_deband_strong_*_bf16.safetensors` | DeBand Strong (experimental), see §1 |
|
| 161 |
+
| `web/` | Static pages: `index.html` (all fixes side by side, colour view, leaderboard), `edit_anatomy.html`, `learnings.html` |
|
| 162 |
+
| `research/` | Tools, benchmark prompts, lab node, every score row (`research/scores/scores.jsonl`); see `research/README.md` |
|
loras/moss_image_sft_deband_strong_full_bf16.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:702867c098e8da5a3fa9bc9fb881896b1d1a511eead45c8d45a4fb4da7a4d8f3
|
| 3 |
+
size 4756351016
|
research/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research tools for MOSS-Image SFT DeBand
|
| 2 |
+
|
| 3 |
+
Everything used to measure and build the fixes in this repo. The findings are in [../RESEARCH.md](../RESEARCH.md); the interactive comparison is in [../web/](../web/index.html).
|
| 4 |
+
|
| 5 |
+
## Setup
|
| 6 |
+
|
| 7 |
+
1. Copy this folder's scripts to `ComfyUI/workflows/moss/` (they find ComfyUI two levels up) and `moss_block_lab/` to `ComfyUI/custom_nodes/moss_block_lab/`, then restart ComfyUI.
|
| 8 |
+
2. Models: `moss_image_sft_bf16.safetensors`, `moss_image_base_bf16.safetensors`, `qwen3.5_9b_int8_convrot.safetensors` and `flux2-vae.safetensors` from [Comfy-Org/MOSS-Image](https://huggingface.co/Comfy-Org/MOSS-Image).
|
| 9 |
+
3. Scoring needs `numpy`, `scikit-image`, `transformers` (DINOv2-base) and `insightface` (buffalo_l, CPU is fine).
|
| 10 |
+
|
| 11 |
+
## The benchmark ("screen")
|
| 12 |
+
|
| 13 |
+
`drift_runner.py <arm> [options]` queues 13 items through ComfyUI's API with the stock preset workflow (`moss_banding_fix_presets_api.json`, preset 5 = plain SFT, ER-SDE, 25 steps, CFG 3, SLG 22/24, TSR): 3 solid colours (grey, blue, beige), 2 banding-bench scenes at 768×1024, 3 people, 1 anime, 2 animals, 1 typography, 1 interior. Renders land in `output/moss/drift/<arm>/<item>__s<seed>_00001_.png`.
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
python3 drift_runner.py sft # reference
|
| 17 |
+
python3 drift_runner.py base --unet moss_image_base_bf16.safetensors
|
| 18 |
+
python3 drift_runner.py mylora --lora my_fix.safetensors:1.0
|
| 19 |
+
python3 drift_runner.py mylora --lora my_fix.safetensors:1.0 --seeds 14 --only solid_grey,solid_blue,solid_beige,p_young_woman_east_asian
|
| 20 |
+
python3 drift_runner.py solve --solve 0.3,0.05,21-41,1,,,genc # in-memory Gram solve (needs the Grams below)
|
| 21 |
+
python3 drift_runner.py cfg25 --cfg 2.5 --no-slg --no-tsr # other sampler settings (score against an SFT arm with the same settings)
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
`drift_score.py <ref_arm> <arm> [...]` pairs images by item and seed and appends rows to `output/moss/drift/scores.jsonl`:
|
| 25 |
+
|
| 26 |
+
| Field | Meaning |
|
| 27 |
+
|---|---|
|
| 28 |
+
| `hf_db` | Solids only. Orientation-free colour-detail power (periods under 40 px, centre 512² crop, R−G and B−(R+G)/2) relative to the reference, in dB. Lower = cleaner. The primary stripe metric. |
|
| 29 |
+
| `band` | `bandviz.local_score`: vertical 48-cycle stripe fingerprint, ≈1.0 = clean. Fooled by rotated residue and real vertical patterns. |
|
| 30 |
+
| `ssim`, `dino_dist` | Overall drift from the reference image. |
|
| 31 |
+
| `arcface` | People only. Identity similarity of the largest face to the reference image's face (1.0 = same person). |
|
| 32 |
+
|
| 33 |
+
`build_compare.py` turns `scores.jsonl` into the comparison page (any number of columns, colour view, sortable leaderboard). `--public <dir> --arms a,b,c` writes a hostable copy with WebP images. `auto_score.py <arms...>` scores and rebuilds every 10 minutes until the ComfyUI queue is empty.
|
| 34 |
+
|
| 35 |
+
## Closed-form Gram solve
|
| 36 |
+
|
| 37 |
+
The lab node `MOSSGramCapture` records, for each linear in layers 21–41, the Gram matrix of its inputs (Σ x xᵀ) over every model call of a render. Options: `image_centered` (image tokens only, each row's mean along the width removed) and `sigma_max` (only late steps). `MOSSGramSave` writes them to `output/moss/grams/<tag>.safetensors` (8 GB for 21–41).
|
| 38 |
+
|
| 39 |
+
`gram_runner.py <src> <tag>` replays a capture prompt set under a new tag (`--image-centered`, `--sigma-max`, `--lora`, `--solve` to capture on an edited model). The captures used here are 36 solid-colour prompts (`flat`) and 36 general prompts (`gen`) at 1024², 1216×832 and 832×1216; the prompts are embedded in the capture PNGs that `MOSSGramSave` runs alongside.
|
| 40 |
+
|
| 41 |
+
`MOSSGramSolve` (in-memory) and `export_gramsolve.py` (to a LoRA file) compute, per linear, with Δ = W_Base − W_SFT:
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
D = Δ · G_num · (G_flat + μ·G_gen + ε·I)⁻¹ ε = ρ · tr(G_flat + μ·G_gen) / d
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
`G_num` defaults to `G_flat` (match Base on solid-colour inputs); `num_tag` / `!num_tag` target only a captured part or its complement. DeBand Strong is `μ=0.3, ρ=0.05, gen=genc` (the general Gram with the per-row colour/brightness removed, so it protects detail without blocking the fix). `build_lowrank.py <file> <ranks...>` makes truncated-SVD LoRAs from any full-difference file.
|
| 48 |
+
|
| 49 |
+
## Edit anatomy
|
| 50 |
+
|
| 51 |
+
`edit_anatomy.py` (+ `edit_anatomy_c.py` for the horizontal-detail measure) computes, for each fix and layer, the weight-change size relative to a full revert and the output-change energy tr(D·G·Dᵀ) on solid-colour vs general activations; `build_anatomy.py` draws it. Measure selectivity on the centred Grams: on raw Grams one per-token colour/brightness/modulation direction holds about half the energy and makes any edit look large.
|
| 52 |
+
|
| 53 |
+
## Training (negative result, kept for reproduction)
|
| 54 |
+
|
| 55 |
+
`trainset_runner.py`, `build_trainset.py`, `train_runner.py` drive ComfyUI's `TrainLoraNode` on MOSS. Needed: run the trainer instance with `--disable-comfy-compiler`, `checkpoint_depth=2` at 1024², and a training-path dtype fix in `comfy/ldm/moss_image/model.py` (q/k cast back to the input dtype after norm + RoPE). A rank-32 de-band LoRA did nothing at 200 steps and degraded images at 600 (see RESEARCH.md).
|
| 56 |
+
|
| 57 |
+
The 72 capture prompts (with sizes and seeds) ship as 128 px stand-in PNGs in `gram_capture/` that carry each original render's embedded API prompt; copy them to `ComfyUI/output/moss/gram_capture/` and `gram_runner.py flat <tag>` / `gram_runner.py gen <tag>` will replay them exactly.
|
research/anatomy_template.html
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
| 3 |
+
<title>DeBand Edit Anatomy</title>
|
| 4 |
+
<style>
|
| 5 |
+
:root{--bg:#f6f6f4;--panel:#fff;--ink:#1d1d1f;--ink2:#52514e;--muted:#898781;--grid:#e9e8e3;--axis:#c3c2b7;--line:#dedede;--accent:#2f6fde;--ring:#fff;
|
| 6 |
+
--s1:#2a78d6;--s2:#eb6834;--s3:#1baf7a;--s4:#eda100;--s5:#e87ba4;--s6:#008300;--s7:#6250d6;--ref:#9a9890}
|
| 7 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#141416;--panel:#1d1d20;--ink:#ececee;--ink2:#c3c2b7;--muted:#898781;--grid:#2c2c2a;--axis:#45454a;--line:#33333a;--accent:#6aa0ff;--ring:#1d1d20;
|
| 8 |
+
--s1:#3987e5;--s2:#d95926;--s3:#199e70;--s4:#c98500;--s5:#d55181;--s6:#008300;--s7:#9085e9;--ref:#6e6d68}}
|
| 9 |
+
:root[data-theme="dark"]{--bg:#141416;--panel:#1d1d20;--ink:#ececee;--ink2:#c3c2b7;--muted:#898781;--grid:#2c2c2a;--axis:#45454a;--line:#33333a;--accent:#6aa0ff;--ring:#1d1d20;
|
| 10 |
+
--s1:#3987e5;--s2:#d95926;--s3:#199e70;--s4:#c98500;--s5:#d55181;--s6:#008300;--s7:#9085e9;--ref:#6e6d68}
|
| 11 |
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
|
| 12 |
+
main{max-width:1180px;margin:0 auto;padding:16px}
|
| 13 |
+
h1{font-size:20px;margin:0 0 4px}h2{font-size:16px;margin:0 0 2px}.sub{color:var(--ink2);font-size:13px;margin:0 0 10px}
|
| 14 |
+
a{color:var(--accent)}
|
| 15 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 14px 10px;margin:0 0 16px}
|
| 16 |
+
.legend{display:flex;flex-wrap:wrap;gap:4px 16px;font-size:12px;color:var(--ink2);margin:6px 0 4px}
|
| 17 |
+
.legend span{display:inline-flex;align-items:center;gap:6px}.legend i{display:inline-block;width:16px;height:2px;border-radius:1px}
|
| 18 |
+
.legend i.dot{width:8px;height:8px;border-radius:50%}
|
| 19 |
+
.chart{position:relative;width:100%}
|
| 20 |
+
svg{display:block;width:100%;overflow:visible}
|
| 21 |
+
svg text{font:11px system-ui,sans-serif;fill:var(--muted)}svg text.lab{fill:var(--ink2);font-size:12px}
|
| 22 |
+
.tip{position:absolute;pointer-events:none;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:6px 10px;font-size:12px;box-shadow:0 2px 10px rgba(0,0,0,.15);display:none;min-width:150px;z-index:2}
|
| 23 |
+
.tip .h{color:var(--ink2);margin-bottom:2px}.tip .r{display:flex;align-items:center;gap:6px;white-space:nowrap}.tip .r b{font-variant-numeric:tabular-nums;min-width:42px}
|
| 24 |
+
.tip .r i{display:inline-block;width:12px;height:2px}
|
| 25 |
+
.multi{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}@media (max-width:900px){.multi{grid-template-columns:1fr}}
|
| 26 |
+
.multi h3{font-size:13px;margin:0;display:flex;align-items:center;gap:6px}
|
| 27 |
+
.note{color:var(--ink2);font-size:13px}.note p{margin:6px 0}
|
| 28 |
+
details summary{cursor:pointer;font-weight:600}
|
| 29 |
+
.tbl{overflow-x:auto}table{border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums}th,td{padding:3px 8px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap}th{color:var(--ink2)}th:first-child,td:first-child{text-align:left}
|
| 30 |
+
</style></head><body><main>
|
| 31 |
+
<h1>What each DeBand fix changes inside MOSS-Image SFT</h1>
|
| 32 |
+
<p class="sub">Computed from the actual weights (Base, SFT, each fix) and the activations recorded during 36 solid-colour and 36 general-prompt SFT renders. Back to the <a href="compare_solve.html">image comparison</a>.</p>
|
| 33 |
+
|
| 34 |
+
<div class="card">
|
| 35 |
+
<h2>Where each fix changes the weights</h2>
|
| 36 |
+
<p class="sub">Size of each fix's weight change in a layer, as a share of fully reverting that layer to Base (100% = that layer becomes Base). Summed over the layer's 4 matrices (attention qkv and o, MLP up/gate and down); hover for the per-matrix split. x = transformer layer. Not drawn: the reverts, which are 100% on their layers and 0 elsewhere (DeBand 37–41, Quality+ 32–41, exact revert 21–41).</p>
|
| 37 |
+
<div class="legend" id="lg1"></div>
|
| 38 |
+
<div class="chart" id="c1"></div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="card">
|
| 42 |
+
<h2>Selectivity: effect on solid colours vs on general prompts (layers 21–41)</h2>
|
| 43 |
+
<div class="legend" id="mode2"></div>
|
| 44 |
+
<p class="sub">How much each fix changes the layers' outputs, as a share of a full revert's change. Up = more change on the activations SFT produces while drawing solid colours (where the stripes are). Right = more change on ordinary prompts (drift). The grey diagonal is a plain scaled revert, which can't tell the two apart; the further above it a fix sits, the more selective it is.</p>
|
| 45 |
+
<div class="legend" id="lg2"></div>
|
| 46 |
+
<div class="chart" id="c2"></div>
|
| 47 |
+
<div class="note" id="n2"></div>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<div class="card">
|
| 51 |
+
<h2>Per layer: solid colours vs general prompts</h2>
|
| 52 |
+
<p class="sub">Same measure per layer, on horizontal detail when available. Coloured line = solid-colour activations, grey line = general prompts. Where the coloured line sits far above the grey one, the fix changes that layer mostly on stripe-prone inputs.</p>
|
| 53 |
+
<div class="multi" id="c3"></div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div class="card note">
|
| 57 |
+
<h2>How to read this</h2>
|
| 58 |
+
<p><b>Weight size (top chart)</b> shows where a fix acts, not how much it matters: a large change on a direction the model never uses does nothing. The reverts are flat 100% blocks. The Gram solves cover 21–41 but keep only part of each layer's difference. Kijai's LoRA is a rank-512 approximation, so it is partial everywhere and also touches layers 3–4.</p>
|
| 59 |
+
<p><b>Output effect (charts 2–3)</b> weighs the weight change by how often each input direction actually occurs: tr(D·G·Dᵀ), with G the recorded activation statistics. That is the change in each layer's output on real activations, before the later layers react to it. The output energies of the four matrix types are summed, so the large MLP matrices dominate. Only layers 21–41 have recorded activations, so Kijai's layers 3–4 are not counted in charts 2–3.</p>
|
| 60 |
+
<p><b>Rendered results</b> in the tooltips (solid HF dB vs SFT, ArcFace vs SFT) are single-seed means from the image comparison, a sanity check on whether the selectivity shows up in pixels.</p>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="card"><details><summary>Table view (all numbers)</summary><div class="tbl" id="tbl"></div></details></div>
|
| 64 |
+
</main>
|
| 65 |
+
<script>
|
| 66 |
+
const D = __DATA__;
|
| 67 |
+
const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim();
|
| 68 |
+
const col=s=>`var(--s${s})`;
|
| 69 |
+
const pct=v=>v==null?'–':(v*100).toFixed(v<0.1?1:0)+'%';
|
| 70 |
+
const NS='http://www.w3.org/2000/svg';
|
| 71 |
+
function el(tag,attrs,parent){const e=document.createElementNS(NS,tag);for(const k in attrs)e.setAttribute(k,attrs[k]);if(parent)parent.appendChild(e);return e;}
|
| 72 |
+
function txt(parent,x,y,s,attrs={}){const t=el('text',{x,y,...attrs},parent);t.textContent=s;return t;}
|
| 73 |
+
function legend(id,items){const L=document.getElementById(id);L.innerHTML='';for(const [label,color,kind] of items){const s=document.createElement('span');const i=document.createElement('i');if(kind==='dot')i.className='dot';i.style.background=color;s.appendChild(i);s.appendChild(document.createTextNode(label));L.appendChild(s);}}
|
| 74 |
+
function tipRows(tip,head,rows){tip.innerHTML='';const h=document.createElement('div');h.className='h';h.textContent=head;tip.appendChild(h);
|
| 75 |
+
for(const [color,val,label] of rows){const r=document.createElement('div');r.className='r';const i=document.createElement('i');i.style.background=color;const b=document.createElement('b');b.textContent=val;r.append(i,b,document.createTextNode(label));tip.appendChild(r);}}
|
| 76 |
+
function placeTip(tip,box,x,y){tip.style.display='block';const w=tip.offsetWidth,h=tip.offsetHeight,W=box.clientWidth;let lx=x+14;if(lx+w>W)lx=x-w-14;tip.style.left=Math.max(0,lx)+'px';tip.style.top=Math.max(0,y-h/2)+'px';}
|
| 77 |
+
|
| 78 |
+
// generic line chart with crosshair tooltip
|
| 79 |
+
function lineChart(box,{xs,series,yMax,height,xTicks,tipHead,tipExtra,steps}){
|
| 80 |
+
box.innerHTML='';const W=box.clientWidth,H=height,m={l:44,r:12,t:10,b:30},pw=W-m.l-m.r,ph=H-m.t-m.b;
|
| 81 |
+
const svg=el('svg',{viewBox:`0 0 ${W} ${H}`,height:H},box);const tip=document.createElement('div');tip.className='tip';box.appendChild(tip);
|
| 82 |
+
const x0=xs[0],x1=xs[xs.length-1],X=v=>m.l+(v-x0)/(x1-x0||1)*pw,Y=v=>m.t+ph-v/yMax*ph;
|
| 83 |
+
const step=yMax>1.2?0.5:0.25;for(let v=0;v<=yMax+1e-9;v+=step){el('line',{x1:m.l,x2:m.l+pw,y1:Y(v),y2:Y(v),stroke:v===0?'var(--axis)':'var(--grid)','stroke-width':1},svg);txt(svg,m.l-6,Y(v)+4,Math.round(v*100)+'%',{'text-anchor':'end'});}
|
| 84 |
+
for(const t of xTicks)txt(svg,X(t),H-10,t,{'text-anchor':'middle'});
|
| 85 |
+
const half=(X(xs[1])-X(xs[0]))/2;
|
| 86 |
+
for(const s of series){const d=steps?s.ys.map((y,i)=>(i?'L':'M')+Math.max(m.l,X(xs[i])-half).toFixed(1)+','+Y(y).toFixed(1)+'L'+Math.min(m.l+pw,X(xs[i])+half).toFixed(1)+','+Y(y).toFixed(1)).join(''):s.ys.map((y,i)=>(i?'L':'M')+X(xs[i]).toFixed(1)+','+Y(y).toFixed(1)).join('');el('path',{d,fill:'none',stroke:s.color,'stroke-width':2,'stroke-linejoin':'round','stroke-linecap':'round'},svg);}
|
| 87 |
+
const cross=el('line',{y1:m.t,y2:m.t+ph,stroke:'var(--axis)','stroke-width':1,visibility:'hidden'},svg);
|
| 88 |
+
const dots=series.map(s=>el('circle',{r:4,fill:s.color,stroke:'var(--ring)','stroke-width':2,visibility:'hidden'},svg));
|
| 89 |
+
const hit=el('rect',{x:m.l,y:m.t,width:pw,height:ph,fill:'transparent',tabindex:0},svg);
|
| 90 |
+
const show=i=>{const x=X(xs[i]);cross.setAttribute('x1',x);cross.setAttribute('x2',x);cross.setAttribute('visibility','visible');
|
| 91 |
+
series.forEach((s,k)=>{dots[k].setAttribute('cx',x);dots[k].setAttribute('cy',Y(s.ys[i]));dots[k].setAttribute('visibility','visible');});
|
| 92 |
+
const rows=series.map(s=>[s.color,pct(s.ys[i]),s.name+(tipExtra?tipExtra(s,i):'')]).sort((a,b)=>parseFloat(b[1])-parseFloat(a[1]));tipRows(tip,tipHead(xs[i]),rows);placeTip(tip,box,x,m.t+ph/2);};
|
| 93 |
+
let cur=0;hit.addEventListener('pointermove',e=>{const r=svg.getBoundingClientRect();const v=x0+(e.clientX-r.left-m.l)/pw*(x1-x0);cur=Math.max(0,Math.min(xs.length-1,Math.round((v-x0)/((x1-x0)/(xs.length-1)))));show(cur);});
|
| 94 |
+
hit.addEventListener('pointerleave',()=>{tip.style.display='none';cross.setAttribute('visibility','hidden');dots.forEach(d=>d.setAttribute('visibility','hidden'));});
|
| 95 |
+
hit.addEventListener('focus',()=>show(cur));hit.addEventListener('keydown',e=>{if(e.key==='ArrowRight')show(cur=Math.min(xs.length-1,cur+1));if(e.key==='ArrowLeft')show(cur=Math.max(0,cur-1));});
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function chart1(){const box=document.getElementById('c1');
|
| 99 |
+
const arms=D.arms.filter(([k])=>!k.startsWith('revert'));
|
| 100 |
+
const series=arms.map(([k,l,s])=>({key:k,name:l,color:col(s),ys:D.size[k]}));
|
| 101 |
+
legend('lg1',arms.map(([k,l,s])=>[l,col(s),'line']));
|
| 102 |
+
const yMax=Math.max(1,...series.flatMap(s=>s.ys))*1.05;
|
| 103 |
+
lineChart(box,{xs:D.layers,series,yMax,height:300,steps:true,xTicks:[0,5,10,15,20,25,30,35,41],tipHead:L=>`Layer ${L}`,
|
| 104 |
+
tipExtra:(s,i)=>{const pm=D.per_matrix[s.key][D.layers[i]];return pm&&s.ys[i]>0?` (qkv ${pct(pm.qkv)}, o ${pct(pm.o)}, up ${pct(pm.ffn_up_gate)}, down ${pct(pm.ffn_down)})`:'';}});
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
let mode2=D.centred?'c':'a';
|
| 108 |
+
function modeButtons(){const m=document.getElementById('mode2');m.textContent='';if(!D.centred)return;m.appendChild(document.createTextNode('Measured on: '));
|
| 109 |
+
for(const [k,l] of [['c','horizontal detail only (where stripes live)'],['a','all activations (dominated by overall colour/brightness)']]){const b=document.createElement('button');b.textContent=l;b.style.cssText='font:inherit;font-size:12px;padding:2px 8px;border-radius:6px;cursor:pointer;background:var(--panel);color:'+(mode2===k?'var(--accent)':'var(--ink2)')+';border:1px solid '+(mode2===k?'var(--accent)':'var(--line)');b.onclick=()=>{mode2=k;chart2();};m.appendChild(b);}}
|
| 110 |
+
function chart2(){modeButtons();const box=document.getElementById('c2');box.innerHTML='';const C=mode2==='c';const px=p=>C?p.xc:p.x,py=p=>C?p.yc:p.y;
|
| 111 |
+
const W=box.clientWidth,size=Math.min(W,560),H=size,m={l:52,r:16,t:12,b:40},pw=W-m.l-m.r,ph=H-m.t-m.b;
|
| 112 |
+
const svg=el('svg',{viewBox:`0 0 ${W} ${H}`,height:H},box);const tip=document.createElement('div');tip.className='tip';box.appendChild(tip);
|
| 113 |
+
const X=v=>m.l+v*pw,Y=v=>m.t+ph-v*ph;
|
| 114 |
+
for(let v=0;v<=1.0001;v+=0.25){el('line',{x1:m.l,x2:m.l+pw,y1:Y(v),y2:Y(v),stroke:v===0?'var(--axis)':'var(--grid)','stroke-width':1},svg);txt(svg,m.l-6,Y(v)+4,Math.round(v*100)+'%',{'text-anchor':'end'});
|
| 115 |
+
el('line',{y1:m.t,y2:m.t+ph,x1:X(v),x2:X(v),stroke:v===0?'var(--axis)':'var(--grid)','stroke-width':1},svg);txt(svg,X(v),m.t+ph+16,Math.round(v*100)+'%',{'text-anchor':'middle'});}
|
| 116 |
+
txt(svg,m.l+pw,H-4,'change on general prompts (drift) →',{'text-anchor':'end'});
|
| 117 |
+
const yl=txt(svg,0,0,'change on solid-colour activations →',{'text-anchor':'end',transform:`translate(12,${m.t}) rotate(-90)`});
|
| 118 |
+
el('line',{x1:X(0),y1:Y(0),x2:X(1),y2:Y(1),stroke:'var(--ref)','stroke-width':2},svg);
|
| 119 |
+
txt(svg,X(0.62),Y(0.55),'plain scaled revert',{'text-anchor':'start',class:'lab',transform:`rotate(${-Math.atan2(ph,pw)*180/Math.PI} ${X(0.62)} ${Y(0.55)})`});
|
| 120 |
+
const sw=C?[]:D.sweep;if(sw.length)el('path',{d:sw.map((p,i)=>(i?'L':'M')+X(p.x).toFixed(1)+','+Y(p.y).toFixed(1)).join(''),fill:'none',stroke:'var(--ink2)','stroke-width':1,opacity:0.6},svg);
|
| 121 |
+
const targets=[];
|
| 122 |
+
for(const p of sw){const c=el('circle',{cx:X(p.x),cy:Y(p.y),r:3,fill:'var(--ink2)'},svg);targets.push({p,x:X(p.x),y:Y(p.y),kind:'sweep',color:'var(--ink2)'});
|
| 123 |
+
if([0.01,10].includes(p.rho))txt(svg,X(p.x)+8,Y(p.y)+4,'ρ='+p.rho,{});}
|
| 124 |
+
if(D.plain){const p={x:0.55,y:0.55,w:0.55,label:'Plain revert ×0.55 (rendered control)',rendered:D.plain};el('circle',{cx:X(.55),cy:Y(.55),r:4,fill:'var(--ref)',stroke:'var(--ring)','stroke-width':2},svg);targets.push({p,x:X(.55),y:Y(.55),kind:'ref',color:'var(--ref)'});}
|
| 125 |
+
for(const p0 of D.points){if(py(p0)==null)continue;const p={...p0,x:px(p0),y:py(p0)};el('circle',{cx:X(p.x),cy:Y(p.y),r:5,fill:col(p.slot),stroke:'var(--ring)','stroke-width':2},svg);targets.push({p,x:X(p.x),y:Y(p.y),kind:'arm',color:col(p.slot)});}
|
| 126 |
+
// direct labels for the arms, nudged off their dot; skip the label if it would collide
|
| 127 |
+
const boxes=targets.map(t=>({x0:t.x-6,x1:t.x+6,y0:t.y-6,y1:t.y+6}));const hitb=(b)=>b.x0<m.l||b.x1>m.l+pw+8||b.y0<m.t-4||b.y1>m.t+ph||boxes.some(q=>b.x0<q.x1&&b.x1>q.x0&&b.y0<q.y1&&b.y1>q.y0);
|
| 128 |
+
for(const t of targets.filter(t=>t.kind==='arm')){const w=t.p.label.length*6.6,h=14;
|
| 129 |
+
const opts=[[t.x+10,t.y+4,'start'],[t.x-10,t.y+4,'end'],[t.x+8,t.y+20,'start'],[t.x-8,t.y-10,'end'],[t.x+8,t.y-10,'start'],[t.x-8,t.y+20,'end']];
|
| 130 |
+
for(const [lx,ly,a] of opts){const b={x0:a==='start'?lx:lx-w,x1:a==='start'?lx+w:lx,y0:ly-11,y1:ly-11+h};if(hitb(b))continue;boxes.push(b);txt(svg,lx,ly,t.p.label,{class:'lab','text-anchor':a});break;}}
|
| 131 |
+
legend('lg2',[...D.points.map(p=>[p.label,col(p.slot),'dot']),...(C?[]:[['Gram solve at other ρ (0.01 → 10)','var(--ink2)','dot']]),['plain scaled revert','var(--ref)','line']]);
|
| 132 |
+
const hit=el('rect',{x:m.l,y:m.t,width:pw,height:ph,fill:'transparent',tabindex:0},svg);let ring=el('circle',{r:8,fill:'none',stroke:'var(--ink)','stroke-width':1.5,visibility:'hidden'},svg);
|
| 133 |
+
const show=t=>{ring.setAttribute('cx',t.x);ring.setAttribute('cy',t.y);ring.setAttribute('visibility','visible');const p=t.p,r=p.rendered;
|
| 134 |
+
const rows=[[t.color,pct(p.y),'change on solid-colour activations'],[t.color,pct(p.x),'change on general prompts'],[t.color,pct(p.w),'weight change (share of full revert)']];
|
| 135 |
+
if(r){if(r.hf!=null)rows.push([t.color,(r.hf>0?'+':'')+r.hf.toFixed(1)+' dB','rendered solids vs SFT'] );if(r.arc!=null)rows.push([t.color,r.arc.toFixed(2),'rendered ArcFace vs SFT']);}
|
| 136 |
+
tipRows(tip,t.kind==='sweep'?`Gram solve ρ=${p.rho}`:p.label,rows);placeTip(tip,box,t.x,t.y);};
|
| 137 |
+
hit.addEventListener('pointermove',e=>{const rc=svg.getBoundingClientRect(),sx=W/rc.width,x=(e.clientX-rc.left)*sx,y=(e.clientY-rc.top)*sx;let best=null,bd=1e9;for(const t of targets){const d=Math.hypot(t.x-x,t.y-y);if(d<bd){bd=d;best=t;}}if(best&&bd<40)show(best);else{tip.style.display='none';ring.setAttribute('visibility','hidden');}});
|
| 138 |
+
hit.addEventListener('pointerleave',()=>{tip.style.display='none';ring.setAttribute('visibility','hidden');});
|
| 139 |
+
let k=0;hit.addEventListener('focus',()=>show(targets[k]));hit.addEventListener('keydown',e=>{if(e.key==='ArrowRight')show(targets[k=(k+1)%targets.length]);if(e.key==='ArrowLeft')show(targets[k=(k+targets.length-1)%targets.length]);});
|
| 140 |
+
const g=k=>{const p=D.points.find(p=>p.key===k);return {x:px(p),y:py(p)};};const p1=g('solve_rho0.1'),p2=g('solve_rho1'),pk=g('kijai_m1');
|
| 141 |
+
const n=document.getElementById('n2');n.textContent='';const para=document.createElement('p');
|
| 142 |
+
para.textContent=`Gram solve ρ=0.1 changes solid-colour outputs by ${pct(p1.y)} of a full revert while changing general-prompt outputs by ${pct(p1.x)}; ρ=1 is ${pct(p2.y)} vs ${pct(p2.x)}; Kijai's LoRA is ${pct(pk.y)} vs ${pct(pk.x)}. A plain revert would be equal on both.`+(C?'':' Caution: on all activations one direction (each token\'s overall colour/brightness plus the timestep modulation) holds about half the energy, so any edit touching it scores high even when it barely changes stripes; switch to horizontal detail.');n.appendChild(para);}
|
| 143 |
+
|
| 144 |
+
function chart3(){const box=document.getElementById('c3');box.innerHTML='';const names={'solve_rho0.1':['Gram solve ρ=0.1',1],'solve_rho1':['Gram solve ρ=1',2],'kijai_m1':['Kijai LoRA @ −1',3]};
|
| 145 |
+
for(const key of Object.keys(D.sel)){const wrap=document.createElement('div');const h=document.createElement('h3');const i=document.createElement('i');i.style.cssText=`display:inline-block;width:16px;height:2px;background:${col(names[key][1])}`;h.append(i,document.createTextNode(names[key][0]));wrap.appendChild(h);
|
| 146 |
+
const c=document.createElement('div');c.className='chart';wrap.appendChild(c);box.appendChild(wrap);
|
| 147 |
+
const s=D.sel[key];const yMax=Math.max(1,...s.solid,...s.gen)*1.05;
|
| 148 |
+
lineChart(c,{xs:D.glayers,series:[{name:'solid-colour activations',color:col(names[key][1]),ys:s.solid},{name:'general prompts',color:'var(--ref)',ys:s.gen}],yMax,height:220,xTicks:[21,25,30,35,41],tipHead:L=>`${names[key][0]} · layer ${L}`});}}
|
| 149 |
+
|
| 150 |
+
function table(){const t=document.getElementById('tbl');const tb=document.createElement('table');const hr=tb.insertRow();
|
| 151 |
+
const hd=['Layer',...D.arms.map(a=>a[1]+' (weight)')];for(const h of hd){const th=document.createElement('th');th.textContent=h;hr.appendChild(th);}
|
| 152 |
+
D.layers.forEach((L,i)=>{const r=tb.insertRow();r.insertCell().textContent=L;for(const [k] of D.arms)r.insertCell().textContent=pct(D.size[k][i]);});
|
| 153 |
+
const h2=document.createElement('h3');h2.textContent='Output change on solid colours vs general prompts (layers 21–41 summed)';
|
| 154 |
+
const tb2=document.createElement('table');const r0=tb2.insertRow();for(const h of ['Fix','solid colours','general prompts','weight','rendered solids dB','rendered ArcFace']){const th=document.createElement('th');th.textContent=h;r0.appendChild(th);}
|
| 155 |
+
for(const p of [...D.points.map(p=>({...p,name:p.label})),...D.sweep.map(p=>({...p,name:'Gram solve ρ='+p.rho}))]){const r=tb2.insertRow();r.insertCell().textContent=p.name;r.insertCell().textContent=pct(p.y);r.insertCell().textContent=pct(p.x);r.insertCell().textContent=pct(p.w);
|
| 156 |
+
r.insertCell().textContent=p.rendered&&p.rendered.hf!=null?p.rendered.hf.toFixed(1):'–';r.insertCell().textContent=p.rendered&&p.rendered.arc!=null?p.rendered.arc.toFixed(2):'–';}
|
| 157 |
+
t.append(tb2,h2,tb);t.insertBefore(h2,tb2);const h1=document.createElement('h3');h1.textContent='Weight change per layer (share of full revert)';t.insertBefore(h1,tb);}
|
| 158 |
+
function all(){chart1();chart2();chart3();}
|
| 159 |
+
table();all();let rt;window.addEventListener('resize',()=>{clearTimeout(rt);rt=setTimeout(all,150);});
|
| 160 |
+
</script></body></html>
|
research/auto_score.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Every few minutes, score drift arms whose render count changed and rebuild compare_solve.html. Stops when the queue is empty.
|
| 3 |
+
|
| 4 |
+
comfyui_venv/bin/python workflows/moss/auto_score.py arm [arm ...]
|
| 5 |
+
"""
|
| 6 |
+
import glob, json, os, subprocess, sys, time, urllib.request
|
| 7 |
+
|
| 8 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
D = os.path.join(HERE, "..", "..", "output", "moss", "drift")
|
| 10 |
+
PY = sys.executable
|
| 11 |
+
arms, seen = sys.argv[1:], {}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def queue_empty():
|
| 15 |
+
try:
|
| 16 |
+
q = json.load(urllib.request.urlopen("http://127.0.0.1:8188/queue"))
|
| 17 |
+
except OSError: # server restarting
|
| 18 |
+
return False
|
| 19 |
+
return not q["queue_running"] and not q["queue_pending"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
while True:
|
| 23 |
+
last = queue_empty()
|
| 24 |
+
changed = [a for a in arms if len(glob.glob(os.path.join(D, a, "*.png"))) != seen.get(a, 0)]
|
| 25 |
+
for a in changed:
|
| 26 |
+
seen[a] = len(glob.glob(os.path.join(D, a, "*.png")))
|
| 27 |
+
if changed:
|
| 28 |
+
out = subprocess.run([PY, os.path.join(HERE, "drift_score.py"), "sft", *changed], capture_output=True, text=True).stdout
|
| 29 |
+
print(time.strftime("%H:%M"), "\n".join(l for l in out.splitlines() if l.startswith(("===", " solid", " people"))), flush=True)
|
| 30 |
+
subprocess.run([PY, os.path.join(HERE, "build_compare.py")])
|
| 31 |
+
if last:
|
| 32 |
+
break
|
| 33 |
+
time.sleep(600)
|
research/bandviz.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Vertical banding score + diagnostic image.
|
| 3 |
+
|
| 4 |
+
comfyui_venv/bin/python workflows/moss/bandviz.py img.png [img2.png ...] [-o outdir]
|
| 5 |
+
|
| 6 |
+
MOSS banding is in color, not brightness, so the score works on two opponent-color channels
|
| 7 |
+
(R-G and B-(R+G)/2): high-pass each row (subtract a 9 px moving average), average down the
|
| 8 |
+
columns, FFT each column profile, sum the two magnitudes, and report strongest peak / median.
|
| 9 |
+
Real vertical bands make one period stand far above the rest. Writes <name>_band.png per image, plus band_compare.png
|
| 10 |
+
when given several images.
|
| 11 |
+
"""
|
| 12 |
+
import argparse, json, os, textwrap
|
| 13 |
+
import numpy as np
|
| 14 |
+
import matplotlib
|
| 15 |
+
matplotlib.use("Agg")
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
from PIL import Image
|
| 18 |
+
|
| 19 |
+
SURFACE, INK, INK2, MUTED, GRID, BLUE = "#fcfcfb", "#0b0b0b", "#52514e", "#898781", "#e1e0d9", "#2a78d6"
|
| 20 |
+
EDGE = 8 # columns dropped at each side; the moving average is invalid there
|
| 21 |
+
BAND = (10, 48) # stripe periods (px) searched for the peak; MOSS bands sit around 16-22 px, 4/8 px is VAE grid texture
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def lora_chain(meta):
|
| 25 |
+
"""(lora_name, strength) for each LoraLoaderModelOnly, ordered from the model loader outward."""
|
| 26 |
+
g = json.loads(meta)
|
| 27 |
+
users = {n["inputs"]["model"][0]: k for k, n in g.items()
|
| 28 |
+
if n["class_type"] == "LoraLoaderModelOnly" and isinstance(n["inputs"].get("model"), list)}
|
| 29 |
+
out, cur = [], next((k for k, n in g.items() if n["class_type"] == "UNETLoader"), None)
|
| 30 |
+
while cur in users:
|
| 31 |
+
cur = users[cur]
|
| 32 |
+
out.append((g[cur]["inputs"]["lora_name"], g[cur]["inputs"]["strength_model"]))
|
| 33 |
+
return out
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def setup_rows(path):
|
| 37 |
+
"""Plain-language generation setup read from the ComfyUI prompt embedded in the PNG, as (label, text) rows."""
|
| 38 |
+
meta = Image.open(path).info.get("prompt")
|
| 39 |
+
if not meta:
|
| 40 |
+
return [("Setup", "generation settings are not embedded in this file")]
|
| 41 |
+
nodes = {n["class_type"]: n["inputs"] for n in json.loads(meta).values()}
|
| 42 |
+
rows = []
|
| 43 |
+
unet = nodes.get("UNETLoader", {}).get("unet_name", "?")
|
| 44 |
+
kind = ("SFT: the fully fine-tuned release (the version that bands)" if "sft" in unet
|
| 45 |
+
else "Base: the pre-fine-tune release" if "base" in unet else "")
|
| 46 |
+
rows.append(("Model", f"MOSS-Image {kind} [{unet}]"))
|
| 47 |
+
loras = lora_chain(meta)
|
| 48 |
+
for i, (n, w) in enumerate(loras):
|
| 49 |
+
what = ("Kijai's SFT-difference LoRA, blocks 21-49 only, rank 512" if "blocks_21_to_49" in n
|
| 50 |
+
else "Kijai's SFT-difference LoRA, all blocks, rank 256" if "rank_256" in n
|
| 51 |
+
else "Kijai's SFT-difference LoRA, all blocks, rank 512" if "rank_512" in n else "LoRA")
|
| 52 |
+
how = ("negative strength subtracts that share of the SFT changes" if w < 0
|
| 53 |
+
else "positive strength adds that share of the SFT changes")
|
| 54 |
+
rows.append((f"LoRA {i + 1}" if len(loras) > 1 else "LoRA", f"{what} at strength {w:+g} ({how}) [{n}]"))
|
| 55 |
+
if not loras:
|
| 56 |
+
rows.append(("LoRA", "none"))
|
| 57 |
+
clip = nodes.get("CLIPLoader")
|
| 58 |
+
if clip:
|
| 59 |
+
rows.append(("Text encoder", clip["clip_name"]))
|
| 60 |
+
samp = ("ER-SDE (eta {eta:g}, noise {s_noise:g}, stages {max_stage})".format(**nodes["SamplerER_SDE"]) if "SamplerER_SDE" in nodes
|
| 61 |
+
else nodes.get("KSamplerSelect", {}).get("sampler_name", "?"))
|
| 62 |
+
sch, sc = nodes.get("BasicScheduler", {}), nodes.get("SamplerCustom", {})
|
| 63 |
+
lat = nodes.get("EmptyFlux2LatentImage", {})
|
| 64 |
+
rows.append(("Sampling", f"sampler {samp}, scheduler {sch.get('scheduler', '?')}, {sch.get('steps', '?')} steps, CFG {sc.get('cfg', '?')}, "
|
| 65 |
+
f"seed {sc.get('noise_seed', '?')}, {lat.get('width', '?')}×{lat.get('height', '?')}"))
|
| 66 |
+
tsr, slg = nodes.get("TemporalScoreRescaling"), nodes.get("SkipLayerGuidanceDiTSimple")
|
| 67 |
+
rows.append(("Model patches", ("Temporal Score Rescaling ON (k {tsr_k:g}, sigma {tsr_sigma:g})".format(**tsr) if tsr else "Temporal Score Rescaling OFF") + " · " +
|
| 68 |
+
("Skip-Layer Guidance ON (layers {single_layers}, {start_percent:.0%}-{end_percent:.0%} of steps)".format(**slg) if slg else "Skip-Layer Guidance OFF")))
|
| 69 |
+
prompt = next((n["inputs"]["text"] for n in json.loads(meta).values()
|
| 70 |
+
if n["class_type"] == "CLIPTextEncode" and isinstance(n["inputs"].get("text"), str) and n["inputs"]["text"].strip()
|
| 71 |
+
and "worst quality" not in n["inputs"]["text"]), None)
|
| 72 |
+
if prompt:
|
| 73 |
+
rows.append(("Prompt", textwrap.shorten(prompt, 230, placeholder=" ...")))
|
| 74 |
+
return rows
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def short_setup(path):
|
| 78 |
+
"""Compact setup label for the comparison chart, e.g. 'SFT + 21-49 LoRA @ -0.5 · ER-SDE'."""
|
| 79 |
+
meta = Image.open(path).info.get("prompt")
|
| 80 |
+
if not meta:
|
| 81 |
+
return os.path.splitext(os.path.basename(path))[0]
|
| 82 |
+
nodes = {n["class_type"]: n["inputs"] for n in json.loads(meta).values()}
|
| 83 |
+
unet = nodes.get("UNETLoader", {}).get("unet_name", "")
|
| 84 |
+
s = "SFT" if "sft" in unet else "Base" if "base" in unet else os.path.splitext(unet)[0]
|
| 85 |
+
for n, w in lora_chain(meta):
|
| 86 |
+
kind = "21-49 LoRA" if "blocks_21_to_49" in n else "r256 LoRA" if "rank_256" in n else "r512 LoRA" if "rank_512" in n else os.path.splitext(n)[0]
|
| 87 |
+
s += f" + {kind} @ {w:+g}"
|
| 88 |
+
if not lora_chain(meta):
|
| 89 |
+
s += ", no LoRA"
|
| 90 |
+
s += " · " + ("ER-SDE" if "SamplerER_SDE" in nodes else nodes.get("KSamplerSelect", {}).get("sampler_name", "?"))
|
| 91 |
+
if "TemporalScoreRescaling" not in nodes:
|
| 92 |
+
s += " · no TSR"
|
| 93 |
+
if "SkipLayerGuidanceDiTSimple" not in nodes:
|
| 94 |
+
s += " · no skip-layer"
|
| 95 |
+
return s
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
CYCLES = 48 # MOSS SFT stripes: ~48 cycles across the image width (16 px at 768, 21.4 px at 1024)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def local_score(path, wobble=6):
|
| 102 |
+
"""Fingerprint banding score that tolerates meandering stripes. 2D spectrum of each opponent-color channel;
|
| 103 |
+
energy within +-wobble vertical-frequency bins (near-vertical stripes) at the known stripe frequency
|
| 104 |
+
(CYCLES across the width, +-1 bin), divided by the median of the same measure at nearby horizontal
|
| 105 |
+
frequencies (+-3..12 bins). About 1 means no stripe."""
|
| 106 |
+
a = np.asarray(Image.open(path).convert("RGB"), dtype=np.float32)
|
| 107 |
+
R, G, B = a[..., 0], a[..., 1], a[..., 2]
|
| 108 |
+
W = a.shape[1]
|
| 109 |
+
prof = 0
|
| 110 |
+
for ch in (R - G, B - (R + G) / 2):
|
| 111 |
+
F = np.abs(np.fft.fft2(ch - ch.mean()))
|
| 112 |
+
prof = prof + np.concatenate([F[:wobble + 1], F[-wobble:]], axis=0).sum(0)[:W // 2]
|
| 113 |
+
k0 = round(CYCLES * W / a.shape[1])
|
| 114 |
+
peak = prof[k0 - 1:k0 + 2].max()
|
| 115 |
+
ring = np.r_[prof[k0 - 12:k0 - 2], prof[k0 + 3:k0 + 13]]
|
| 116 |
+
return float(peak / np.median(ring))
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def analyze(path):
|
| 120 |
+
a = np.asarray(Image.open(path).convert("RGB"), dtype=np.float32)
|
| 121 |
+
R, G, B = a[..., 0], a[..., 1], a[..., 2]
|
| 122 |
+
cols, spec = [], 0
|
| 123 |
+
for ch in (R - G, B - (R + G) / 2):
|
| 124 |
+
hp = ch - np.stack([np.convolve(r, np.ones(9) / 9, mode="same") for r in ch])
|
| 125 |
+
col = hp[:, EDGE:-EDGE].mean(0)
|
| 126 |
+
col = col - col.mean()
|
| 127 |
+
cols.append(col)
|
| 128 |
+
spec = spec + np.abs(np.fft.rfft(col))[1:]
|
| 129 |
+
n = len(cols[0])
|
| 130 |
+
periods = n / np.arange(1, len(spec) + 1)
|
| 131 |
+
in_band = (periods >= BAND[0]) & (periods <= BAND[1])
|
| 132 |
+
k = int(np.flatnonzero(in_band)[spec[in_band].argmax()]) + 1
|
| 133 |
+
chroma = np.clip((a - a.mean(2, keepdims=True)) * 5 + 128, 0, 255).astype(np.uint8)
|
| 134 |
+
return dict(img=a.astype(np.uint8), chroma=chroma, cols=cols, spec=spec, n=n, k=k,
|
| 135 |
+
score=spec[k - 1] / np.median(spec), period=n / k, col_std=float(np.hypot(*[c.std() for c in cols])))
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def style(ax):
|
| 139 |
+
ax.set_facecolor(SURFACE)
|
| 140 |
+
for s in ("top", "right"):
|
| 141 |
+
ax.spines[s].set_visible(False)
|
| 142 |
+
for s in ("left", "bottom"):
|
| 143 |
+
ax.spines[s].set_color(MUTED)
|
| 144 |
+
ax.tick_params(colors=MUTED, labelsize=10)
|
| 145 |
+
ax.grid(True, color=GRID, linewidth=0.8)
|
| 146 |
+
ax.set_axisbelow(True)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def diagnostic(path, r, out):
|
| 150 |
+
"""Pixel-exact layout: image panels are drawn 1:1 at native resolution (dpi 100, 1 px = 1 px)."""
|
| 151 |
+
name = os.path.basename(path)
|
| 152 |
+
H, W = r["img"].shape[:2]
|
| 153 |
+
M, GAP, HEAD, LBL, STRIP, PLOT = 70, 70, 330, 92, 56, 560
|
| 154 |
+
FW = 2 * W + GAP + 2 * M
|
| 155 |
+
FH = HEAD + LBL + H + LBL + STRIP + GAP + LBL + PLOT + 60
|
| 156 |
+
fig = plt.figure(figsize=(FW / 100, FH / 100), dpi=100, facecolor=SURFACE)
|
| 157 |
+
|
| 158 |
+
def add(x, y, w, h): # top-left pixel coordinates -> figure axes
|
| 159 |
+
return fig.add_axes([x / FW, 1 - (y + h) / FH, w / FW, h / FH])
|
| 160 |
+
|
| 161 |
+
def text(x, y, s, size, color, **kw): # y = top of the text, in pixels
|
| 162 |
+
fig.text(x / FW, 1 - y / FH, s, color=color, fontsize=size, va="top", **kw)
|
| 163 |
+
|
| 164 |
+
def label(x, y, title, hint):
|
| 165 |
+
text(x, y + 4, title, 14, INK)
|
| 166 |
+
text(x, y + 28, textwrap.fill(hint, max(60, W // 8)), 11.5, INK2, linespacing=1.35)
|
| 167 |
+
|
| 168 |
+
text(M, 22, f"MOSS-Image vertical color banding check · score {r['score']:.1f} · stripe width {r['period']:.1f} px", 22, INK)
|
| 169 |
+
text(M, 62, "Score = how strongly one stripe width repeats across the image, compared with a typical stripe width (peak ÷ median of the spectrum, bottom right). "
|
| 170 |
+
"Higher = more banding.", 12.5, INK2)
|
| 171 |
+
text(M, 84, "Reference on the goose bench (same prompt, seed 11): full SFT model scores 41 and is visibly banded; "
|
| 172 |
+
"SFT + blocks 21-49 LoRA at -0.5 scores about 16 and looks clean.", 12.5, INK2)
|
| 173 |
+
text(M, 122, f"Setup for this image ({name})", 14, INK)
|
| 174 |
+
for i, (k, v) in enumerate(setup_rows(path)):
|
| 175 |
+
text(M, 150 + i * 24, k, 12.5, INK)
|
| 176 |
+
text(M + 150, 150 + i * 24, v, 12.5, INK2)
|
| 177 |
+
|
| 178 |
+
xr = M + W + GAP
|
| 179 |
+
y = HEAD
|
| 180 |
+
label(M, y, "Generated image", "The image exactly as MOSS produced it, shown pixel for pixel with no resizing. Banding is subtle here: faint vertical color stripes, easiest to spot in flat areas like ceilings, walls and skin.")
|
| 181 |
+
label(xr, y, "Color-only view (5× boosted)", "Brightness is removed and the remaining color is boosted 5×, so tiny color shifts become obvious. A banded image shows evenly spaced vertical stripes running top to bottom; a clean one shows only random grain and object edges.")
|
| 182 |
+
y += LBL
|
| 183 |
+
for x, im in ((M, r["img"]), (xr, r["chroma"])):
|
| 184 |
+
ax = add(x, y, W, H)
|
| 185 |
+
ax.imshow(im, interpolation="nearest")
|
| 186 |
+
ax.axis("off")
|
| 187 |
+
y += H
|
| 188 |
+
label(xr, y + 4, "Average color detail of each column (top: red-green, bottom: blue-yellow)", "Each column of the color view is averaged down to one value and lined up pixel for pixel with the view above. A banded image turns into a regular barcode with a steady rhythm; a clean one looks like random static.")
|
| 189 |
+
y += LBL + 4
|
| 190 |
+
c1, c2 = r["cols"]
|
| 191 |
+
m = max(np.abs(c1).max(), np.abs(c2).max()) or 1
|
| 192 |
+
strip = np.concatenate([np.tile(c1, (STRIP // 2, 1)), np.tile(c2, (STRIP // 2, 1))])
|
| 193 |
+
ax = add(xr + EDGE, y, r["n"], STRIP)
|
| 194 |
+
ax.imshow(strip, cmap="gray", vmin=-m, vmax=m, aspect="auto", interpolation="nearest")
|
| 195 |
+
ax.axis("off")
|
| 196 |
+
y += STRIP + GAP
|
| 197 |
+
|
| 198 |
+
label(M, y, "Color detail per column", "The same column averages drawn as a line, where height is how strongly the color shifts in that column. Banding shows up as peaks repeating at a steady spacing across the whole width, not as one-off spikes from objects.")
|
| 199 |
+
label(xr, y, "Which stripe widths repeat (spectrum of the column detail)", "Breaks the column line into repeating patterns and shows how strong each stripe width is. One tall spike inside the shaded 10-48 px range means banding, and the score is that spike's height divided by the dashed median.")
|
| 200 |
+
y += LBL
|
| 201 |
+
ax = add(M + 50, y, W - 50, PLOT - 50)
|
| 202 |
+
style(ax)
|
| 203 |
+
ax.plot(np.arange(EDGE, EDGE + len(c1)), np.hypot(c1, c2), color=BLUE, linewidth=1.2)
|
| 204 |
+
ax.set_xlim(0, W)
|
| 205 |
+
ax.set_xlabel("column (x position in pixels)", color=MUTED, fontsize=11)
|
| 206 |
+
|
| 207 |
+
ax = add(xr + 50, y, W - 50, PLOT - 50)
|
| 208 |
+
style(ax)
|
| 209 |
+
periods = r["n"] / np.arange(1, len(r["spec"]) + 1)
|
| 210 |
+
sel = periods <= 128
|
| 211 |
+
ax.axvspan(*BAND, color=GRID, alpha=0.5, linewidth=0)
|
| 212 |
+
ax.plot(periods[sel], r["spec"][sel], color=BLUE, linewidth=2)
|
| 213 |
+
med = np.median(r["spec"])
|
| 214 |
+
ax.axhline(med, color=MUTED, linewidth=1, linestyle=(0, (4, 3)))
|
| 215 |
+
ax.text(125, med, "median ", color=MUTED, fontsize=11, va="bottom", ha="right")
|
| 216 |
+
pk = r["spec"][r["k"] - 1]
|
| 217 |
+
ax.plot([r["period"]], [pk], "o", markersize=10, color=BLUE, markeredgecolor=SURFACE, markeredgewidth=2)
|
| 218 |
+
ax.annotate(f"score {r['score']:.1f} (stripes every {r['period']:.1f} px)", (r["period"], pk), xytext=(14, -4),
|
| 219 |
+
textcoords="offset points", color=INK, fontsize=13, va="center")
|
| 220 |
+
ax.set_xscale("log", base=2)
|
| 221 |
+
ax.set_xticks([2, 4, 8, 16, 32, 64, 128])
|
| 222 |
+
ax.set_xticklabels(["2", "4", "8", "16", "32", "64", "128"])
|
| 223 |
+
ax.set_xlim(2, 128)
|
| 224 |
+
ax.set_xlabel("stripe width in pixels (log scale); shaded = range searched for banding, 4 and 8 px spikes are normal VAE texture", color=MUTED, fontsize=11)
|
| 225 |
+
ax.set_ylabel("strength", color=MUTED, fontsize=11)
|
| 226 |
+
|
| 227 |
+
fig.savefig(out, dpi=100, facecolor=SURFACE)
|
| 228 |
+
plt.close(fig)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def compare(results, out):
|
| 232 |
+
results = sorted(results, key=lambda t: t[1]["score"])
|
| 233 |
+
fig, ax = plt.subplots(figsize=(13, 0.5 * len(results) + 2.2), facecolor=SURFACE)
|
| 234 |
+
style(ax)
|
| 235 |
+
ax.grid(axis="y", visible=False)
|
| 236 |
+
names = [short_setup(p) for p, _ in results]
|
| 237 |
+
scores = [r["score"] for _, r in results]
|
| 238 |
+
ax.barh(range(len(results)), scores, color=BLUE, height=0.6)
|
| 239 |
+
ax.set_yticks(range(len(results)), names)
|
| 240 |
+
for i, (_, r) in enumerate(results):
|
| 241 |
+
ax.text(r["score"], i, f" {r['score']:.1f}", va="center", color=INK2, fontsize=10)
|
| 242 |
+
ax.set_xlim(0, max(scores) * 1.12)
|
| 243 |
+
ax.tick_params(axis="y", colors=INK2, labelsize=10.5)
|
| 244 |
+
ax.set_xlabel("banding score (peak ÷ median of the column-color spectrum)", color=MUTED, fontsize=10)
|
| 245 |
+
fig.text(0.01, 0.985, "MOSS-Image vertical color banding, higher = more banding", color=INK, fontsize=14, va="top")
|
| 246 |
+
fig.text(0.01, 0.985 - 0.32 / fig.get_figheight(), "Same prompt and seed for every bar. Goose bench reference: full SFT 41 is visibly banded; SFT + 21-49 LoRA @ -0.5 at ~16 looks clean.",
|
| 247 |
+
color=INK2, fontsize=10.5, va="top")
|
| 248 |
+
fig.tight_layout(rect=(0, 0, 1, 1 - 0.75 / fig.get_figheight()))
|
| 249 |
+
fig.savefig(out, dpi=100, facecolor=SURFACE)
|
| 250 |
+
plt.close(fig)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def hf_chroma_power(path):
|
| 254 |
+
"""Total chroma power at periods under 40 px, any orientation (centre 512x512 crop): the orientation-free artifact measure
|
| 255 |
+
for solid colours, where any colour texture is artifact. The vertical-only banding score misses rotated/diagonal residue."""
|
| 256 |
+
a = np.asarray(Image.open(path).convert("RGB")).astype(np.float32)
|
| 257 |
+
H, W = a.shape[:2]
|
| 258 |
+
a = a[H // 2 - 256:H // 2 + 256, W // 2 - 256:W // 2 + 256]
|
| 259 |
+
win = np.outer(np.hanning(512), np.hanning(512))
|
| 260 |
+
k = np.hypot(*np.meshgrid(np.fft.fftfreq(512) * 512, np.fft.fftfreq(512) * 512))
|
| 261 |
+
total = 0.0
|
| 262 |
+
for ch in (a[..., 0] - a[..., 1], a[..., 2] - (a[..., 0] + a[..., 1]) / 2):
|
| 263 |
+
total += float((np.abs(np.fft.fft2((ch - ch.mean()) * win)) ** 2)[k >= 512 / 40].sum())
|
| 264 |
+
return total
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def main():
|
| 268 |
+
p = argparse.ArgumentParser()
|
| 269 |
+
p.add_argument("images", nargs="+")
|
| 270 |
+
p.add_argument("-o", "--outdir")
|
| 271 |
+
p.add_argument("--no-plots", action="store_true", help="print scores only")
|
| 272 |
+
a = p.parse_args()
|
| 273 |
+
results = []
|
| 274 |
+
for path in a.images:
|
| 275 |
+
r = analyze(path)
|
| 276 |
+
results.append((path, r))
|
| 277 |
+
print(f"{os.path.basename(path):28s} score={r['score']:5.1f} period={r['period']:5.1f}px col_std={r['col_std']:.3f}")
|
| 278 |
+
if not a.no_plots:
|
| 279 |
+
outdir = a.outdir or os.path.dirname(os.path.abspath(path))
|
| 280 |
+
diagnostic(path, r, os.path.join(outdir, os.path.splitext(os.path.basename(path))[0] + "_band.png"))
|
| 281 |
+
if len(results) > 1 and not a.no_plots:
|
| 282 |
+
compare(results, os.path.join(a.outdir or os.path.dirname(os.path.abspath(a.images[0])), "band_compare.png"))
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
if __name__ == "__main__":
|
| 286 |
+
main()
|
research/bench/drift_suite_v1.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id": "p_elder_man_west_african", "category": "people", "prompt": "A close-up portrait photograph of a cheerful elderly West African man in his seventies with short white hair and a neatly trimmed white beard, wearing a patterned kufi cap and a deep green shirt, smiling warmly at the camera. Soft window light from the left, plain warm grey background, 85mm lens, natural skin texture."},
|
| 3 |
+
{"id": "p_young_woman_east_asian", "category": "people", "prompt": "A close-up portrait photograph of a young East Asian woman in her twenties with straight black shoulder-length hair and light makeup, wearing a cream turtleneck sweater, looking at the camera with a calm expression. Soft studio light, plain light grey background, 85mm lens, natural skin texture."},
|
| 4 |
+
{"id": "p_mid_man_south_asian", "category": "people", "prompt": "A head-and-shoulders portrait photograph of a South Asian man in his forties with a short black beard and glasses, wearing a navy blazer over a white shirt, slight smile. Even daylight from a large window, blurred office background, 85mm lens."},
|
| 5 |
+
{"id": "p_teen_girl_latina", "category": "people", "prompt": "A portrait photograph of a Latina teenage girl around sixteen with curly dark brown hair tied back, wearing a yellow hoodie, laughing and looking slightly off camera. Golden hour sunlight outdoors, soft green park background out of focus."},
|
| 6 |
+
{"id": "p_old_woman_european", "category": "people", "prompt": "A close-up portrait photograph of an elderly European woman in her eighties with silver hair in a loose bun, deep smile lines and kind blue eyes, wearing a knitted lavender cardigan and pearl earrings. Soft window light, warm beige background."},
|
| 7 |
+
{"id": "p_child_boy_black", "category": "people", "prompt": "A portrait photograph of a happy Black boy about seven years old with short curly hair and a gap-toothed grin, wearing a red striped t-shirt, holding a toy airplane. Bright soft daylight, plain pale blue wall behind him."},
|
| 8 |
+
{"id": "p_man_middle_eastern", "category": "people", "prompt": "A portrait photograph of a Middle Eastern man in his thirties with dark wavy hair and a short beard, wearing a denim jacket, serious expression, looking straight at the camera. Overcast outdoor light, blurred city street behind him."},
|
| 9 |
+
{"id": "p_woman_nordic_freckles", "category": "people", "prompt": "A close-up portrait photograph of a Nordic woman in her thirties with strawberry blonde hair, many freckles and green eyes, wearing a chunky grey wool scarf, soft smile. Cold diffuse winter daylight, snowy background out of focus."},
|
| 10 |
+
{"id": "a_anime_girl_school", "category": "anime_people", "prompt": "A clean anime illustration of a cheerful high school girl with short teal hair and a yellow hairclip, wearing a navy sailor uniform with a red ribbon, waving at the viewer on a sunny rooftop with a chain-link fence and blue sky behind her. Cel shading, crisp line art."},
|
| 11 |
+
{"id": "a_anime_old_wizard", "category": "anime_people", "prompt": "An anime illustration of a kind old wizard with a long white beard, round glasses and a tall blue hat with silver stars, reading a glowing book in a cozy library full of shelves. Soft warm lamplight, detailed background art, cel shading."},
|
| 12 |
+
{"id": "a_anime_boy_hero", "category": "anime_people", "prompt": "An anime illustration of a determined teenage boy with spiky black hair and a red scarf, holding a wooden sword, standing on a grassy hill at sunset with windblown clouds behind him. Dynamic composition, clean line art, vivid colors."},
|
| 13 |
+
{"id": "a_anime_chibi_baker", "category": "anime_people", "prompt": "A cute chibi anime illustration of a small baker girl with pink twin tails and a white apron, proudly holding a tray of strawberry cupcakes in a pastel bakery. Big sparkling eyes, soft pastel colors, simple clean background."},
|
| 14 |
+
{"id": "x_golden_retriever", "category": "animals", "prompt": "A photograph of a golden retriever sitting on a wooden porch, tongue out and looking happily at the camera, soft afternoon light, blurred garden behind."},
|
| 15 |
+
{"id": "x_tabby_cat_window", "category": "animals", "prompt": "A photograph of an orange tabby cat curled up on a sunny windowsill, eyes half closed, sunlight catching its whiskers and fur, potted plants beside it."},
|
| 16 |
+
{"id": "x_red_fox_snow", "category": "animals", "prompt": "A wildlife photograph of a red fox standing in fresh snow in a birch forest, looking toward the camera, its breath visible in the cold air, soft overcast light, telephoto lens."},
|
| 17 |
+
{"id": "x_parrot_macaw", "category": "animals", "prompt": "A close-up wildlife photograph of a scarlet macaw perched on a mossy branch in a rainforest, vivid red, yellow and blue feathers in sharp detail, dark green bokeh background."},
|
| 18 |
+
{"id": "x_elephant_savanna", "category": "animals", "prompt": "A wildlife photograph of an African elephant mother and calf walking across a golden savanna at sunset, dust in the air, acacia trees on the horizon."},
|
| 19 |
+
{"id": "x_horse_field", "category": "animals", "prompt": "A photograph of a dark brown horse with a white blaze grazing in a misty green meadow at dawn, wooden fence in the background, soft golden light."},
|
| 20 |
+
{"id": "t_cafe_chalkboard", "category": "typography", "prompt": "A photograph of a cafe chalkboard menu on a brick wall that reads \"TODAY'S SPECIALS\" at the top, then \"Oat Latte 4.50\" and \"Blueberry Scone 3.25\" in neat white chalk handwriting. No other text."},
|
| 21 |
+
{"id": "t_neon_sign", "category": "typography", "prompt": "A night photograph of a pink neon sign on a dark wall that reads \"OPEN LATE\" in flowing script letters, glowing reflections on wet pavement below. No other text."},
|
| 22 |
+
{"id": "t_book_cover", "category": "typography", "prompt": "A minimalist book cover design with a deep blue background, a single white paper boat in the center, the title \"THE QUIET SEA\" in large elegant serif letters at the top and \"A Novel\" in small letters at the bottom. No other text."},
|
| 23 |
+
{"id": "o_watch_macro", "category": "objects", "prompt": "A macro product photograph of a mechanical wristwatch with an open skeleton dial showing tiny gears and jewels, brushed steel case, on a dark slate surface with soft studio lighting."},
|
| 24 |
+
{"id": "o_kitchen_scene", "category": "scenes", "prompt": "A photograph of a bright Scandinavian kitchen with white cabinets, a wooden table set with ceramic mugs and a bowl of lemons, sunlight streaming through a large window, plants on the counter."},
|
| 25 |
+
{"id": "o_city_street_rain", "category": "scenes", "prompt": "A street photograph of a busy European city street in light rain at dusk, people with umbrellas, warm shop windows reflecting on the wet cobblestones, a red tram in the distance."}
|
| 26 |
+
]
|
research/bench/prompts_v1.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id": "goose", "tests": "Original bench: busy scene, signage text, continuity with earlier results",
|
| 3 |
+
"prompt": "A blurry, crooked photograph of a goose standing on a supermarket checkout conveyor belt holding a single loaf of bread in its beak, staring dead at the camera with menacing intensity, while the cashier and a long line of shoppers behind freeze in silence. A sign above the register reads \"EXPRESS LANE: 10 ITEMS OR LESS\". Overhead fluorescent lighting, harsh shadows, cheap phone camera noise."},
|
| 4 |
+
|
| 5 |
+
{"id": "studio_portrait", "tests": "Skin, hair detail, and a big flat seamless backdrop (banding magnet)",
|
| 6 |
+
"prompt": "A studio portrait photograph of a woman in her thirties with short silver hair and freckles, wearing a charcoal wool turtleneck, seated against a plain warm-grey seamless paper backdrop that fills most of the frame. Soft single key light from the upper left with a large octabox, gentle falloff into shadow on the right side of her face. Shot on a medium format camera, 80mm lens, shallow depth of field, natural skin texture, calm direct gaze into the lens."},
|
| 7 |
+
|
| 8 |
+
{"id": "white_interior", "tests": "Flat white walls and soft light gradients",
|
| 9 |
+
"prompt": "An architectural interior photograph of a minimalist living room with smooth white plaster walls, a pale oak floor and a single low linen sofa. Late afternoon sunlight enters through a tall window on the left and casts a long soft-edged rectangle of light across the wall and floor. Large areas of empty wall, quiet and calm, neutral colors, wide angle lens, straight verticals, high dynamic range."},
|
| 10 |
+
|
| 11 |
+
{"id": "fjord_sky", "tests": "Large smooth sky gradient and still water reflections",
|
| 12 |
+
"prompt": "A landscape photograph of a Norwegian fjord at blue hour, the sky a smooth gradient from deep indigo at the top to pale peach just above the mountains. Snow-capped dark mountains line both sides, and the perfectly still water mirrors the sky and peaks. A single small red wooden cabin with a lit window sits on the right shore. Long exposure, no clouds, crisp and quiet."},
|
| 13 |
+
|
| 14 |
+
{"id": "fog_forest", "tests": "Low-contrast haze, subtle tonal gradients",
|
| 15 |
+
"prompt": "A photograph of a dense pine forest in thick morning fog, rows of tall trunks fading from sharp dark silhouettes in the foreground to pale grey shapes in the distance. Soft diffuse light, almost monochrome blue-grey tones with a faint warm glow where the sun tries to break through at the top right. A narrow dirt path leads from the bottom center into the fog."},
|
| 16 |
+
|
| 17 |
+
{"id": "product_glass", "tests": "Pastel gradient backdrop, glass, caustics, small label text",
|
| 18 |
+
"prompt": "A luxury product photograph of a faceted glass perfume bottle filled with pale amber liquid, standing on a glossy surface in front of a smooth pastel backdrop that blends from soft lilac at the top to mint green at the bottom. Bright caustic reflections fall on the surface beside it. A small minimalist label on the bottle reads \"NORDLYS\" in thin gold capital letters. Studio lighting, ultra clean, sharp focus."},
|
| 19 |
+
|
| 20 |
+
{"id": "poster_text", "tests": "Dense typography and layout, MOSS's text strength",
|
| 21 |
+
"prompt": "A Swiss-style concert poster with a flat cream background. At the top in large bold black sans-serif letters it reads \"RIDING THE BIFROST\". Below, a large abstract rainbow arc made of thick flat stripes of aqua, teal, tangerine and electric blue. At the bottom left in smaller text: \"LIVE AT THE SKY PLATFORM\" and under it \"FRIDAY 21:00\". At the bottom right a small line reads \"TICKETS AT THE GATE\". Clean grid layout, generous margins, flat print colors."},
|
| 22 |
+
|
| 23 |
+
{"id": "mascot_2d", "tests": "Flat 2D color fills, clean line art (project mascot)",
|
| 24 |
+
"prompt": "A flat 2D vector illustration of a cute kawaii sea serpent mascot named Jormungandr, coiled in a loose loop with its tail in its mouth, with big round sparkly eyes, rosy cheeks and a tiny smile. Hot pink and pastel lavender body with soft mint belly scales drawn as simple flat shapes, bold clean black outlines, no shading gradients. Plain flat pale yellow background, sticker-style character sheet design, centered."},
|
| 25 |
+
|
| 26 |
+
{"id": "angel_rave", "tests": "Project hero look: neon night, aurora sky, skin, hair, gold detail",
|
| 27 |
+
"prompt": "A cinematic photograph of a 21-year-old Scandinavian woman with long wavy blonde hair, dressed as a kawaii angel with a glowing gold halo and large white feathered wings, dancing on a futuristic open-air sky rave platform at night. Behind her, a vivid green and teal aurora ripples across a starry sky. She wears an aqua and teal festival outfit with small gold accents. Electric blue and tangerine stage lights, light haze, joyful expression, 35mm lens, shallow depth of field."},
|
| 28 |
+
|
| 29 |
+
{"id": "neon_rain", "tests": "Dark low-key scene, saturated light, gradients in shadow",
|
| 30 |
+
"prompt": "A night street photograph in a narrow alley in Tokyo during light rain, wet asphalt reflecting pink and cyan neon signs. A lone person with a clear umbrella walks away from the camera in the middle distance. Most of the frame is deep shadow with soft glowing haze around the signs. One vertical sign on the right reads \"喫茶\" in glowing red. Shot on film, slight grain, moody and quiet."},
|
| 31 |
+
|
| 32 |
+
{"id": "macro_moss", "tests": "Fine texture and sharpness, to catch detail loss from a fix",
|
| 33 |
+
"prompt": "An extreme macro photograph of bright green moss on a grey granite stone, covered in tiny dew droplets that each reflect the morning sky. Individual moss leaves and fine hair-like stalks with small brown spore capsules are in sharp focus in the center, falling off into creamy bokeh at the edges. Soft overcast light, rich natural color, incredible detail."},
|
| 34 |
+
|
| 35 |
+
{"id": "oil_still_life", "tests": "Painterly style, brushwork, dark background",
|
| 36 |
+
"prompt": "An oil painting still life in the style of the Dutch Golden Age: a pewter plate with a peeled lemon, a half-filled glass of white wine, a few green grapes and a crumpled white napkin on a dark wooden table, against a deep brown-black background. Dramatic light from the left, visible brushstrokes and glazing, crackled varnish texture, rich warm tones."},
|
| 37 |
+
|
| 38 |
+
{"id": "hands_dough", "tests": "Hands and anatomy, flour texture, warm skin",
|
| 39 |
+
"prompt": "A close-up documentary photograph of an elderly baker's weathered hands kneading a large ball of bread dough on a flour-dusted wooden counter. Flour clings to the knuckles and fingernails, and a fine cloud of flour drifts in the warm side light from a nearby window. A rolling pin and a small bowl of salt sit slightly out of focus in the background. Natural colors, shallow depth of field."}
|
| 40 |
+
]
|
research/build_anatomy.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build output/moss/edit_anatomy.html from output/moss/edit_anatomy.json (edit_anatomy.py) and drift/scores.jsonl.
|
| 3 |
+
|
| 4 |
+
python3 workflows/moss/build_anatomy.py
|
| 5 |
+
"""
|
| 6 |
+
import json, math, os, statistics
|
| 7 |
+
|
| 8 |
+
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
|
| 9 |
+
OUT = os.path.join(ROOT, "output", "moss")
|
| 10 |
+
rows = json.load(open(os.path.join(OUT, "edit_anatomy.json")))
|
| 11 |
+
|
| 12 |
+
# fixed series slots (colour follows the fix on every chart)
|
| 13 |
+
ARMS = [("solve_rho0.1", "Gram solve ρ=0.1", 1, "solve_mu0_rho0.1_L21-41"),
|
| 14 |
+
("solve_rho1", "Gram solve ρ=1", 2, "solve_mu0_rho1_L21-41"),
|
| 15 |
+
("kijai_m1", "Kijai LoRA @ −1", 3, "kijai_m1"),
|
| 16 |
+
("kijai_m05", "Kijai LoRA @ −0.5", 7, "kijai_m05"),
|
| 17 |
+
("revert21", "Exact revert 21–41", 4, "solve_mu0_L21-41"),
|
| 18 |
+
("revert32", "Quality+ (revert 32–41)", 5, "qualityplus_lora"),
|
| 19 |
+
("revert37", "DeBand (revert 37–41)", 6, "deband_lora")]
|
| 20 |
+
RANGES = {"revert21": (21, 41), "revert32": (32, 41), "revert37": (37, 41)}
|
| 21 |
+
PROJ = ("qkv", "o", "ffn_up_gate", "ffn_down")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
CKPT = {"solve_rho0.1": "ckpt_rho0.1", "solve_rho1": "ckpt_rho1"} # saved checkpoints carry the centred (efc/egc) numbers
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def edit(r, arm, centred=False):
|
| 28 |
+
"""The arm's numbers for one linear, or None where it does not edit."""
|
| 29 |
+
if arm == "kijai_m05": # half strength: energies x 0.25, alignment x 0.5
|
| 30 |
+
k = r.get("kijai_m1")
|
| 31 |
+
return k and {key: v / (2 if key == "dot" else 4) for key, v in k.items()}
|
| 32 |
+
if centred and arm in CKPT:
|
| 33 |
+
return r.get(CKPT[arm])
|
| 34 |
+
if arm in RANGES:
|
| 35 |
+
lo, hi = RANGES[arm]
|
| 36 |
+
return r["revert"] if lo <= r["layer"] <= hi else None
|
| 37 |
+
return r.get(arm)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def ratio(a, b):
|
| 41 |
+
return math.sqrt(a / b) if b > 0 else 0.0
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# rendered results for tooltips
|
| 45 |
+
scores = {}
|
| 46 |
+
for line in open(os.path.join(OUT, "drift", "scores.jsonl")):
|
| 47 |
+
s = json.loads(line)
|
| 48 |
+
scores[(s["arm"], s["id"], s["seed"])] = s
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def rendered(arm):
|
| 52 |
+
rs = [s for (a, _, _), s in scores.items() if a == arm]
|
| 53 |
+
if not rs:
|
| 54 |
+
return None
|
| 55 |
+
hf = [s["hf_db"] for s in rs if s.get("hf_db") is not None]
|
| 56 |
+
af = [s["arcface"] for s in rs if s.get("arcface") is not None]
|
| 57 |
+
return {"hf": statistics.fmean(hf) if hf else None, "arc": statistics.fmean(af) if af else None}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
layers = sorted({r["layer"] for r in rows})
|
| 61 |
+
by = {(r["layer"], r["proj"]): r for r in rows}
|
| 62 |
+
|
| 63 |
+
# chart 1: weight-change size per layer (sqrt of energy over the 4 linears, relative to a full revert of that layer)
|
| 64 |
+
size = {}
|
| 65 |
+
for key, _, _, _ in ARMS:
|
| 66 |
+
size[key] = []
|
| 67 |
+
for L in layers:
|
| 68 |
+
rs = [by[(L, p)] for p in PROJ]
|
| 69 |
+
num = sum(edit(r, key)["w2"] for r in rs if edit(r, key))
|
| 70 |
+
size[key].append(ratio(num, sum(r["revert"]["w2"] for r in rs)))
|
| 71 |
+
per_matrix = {key: {f"{L}": {p: (round(ratio(edit(by[(L, p)], key)["w2"], by[(L, p)]["revert"]["w2"]), 3) if edit(by[(L, p)], key) else 0)
|
| 72 |
+
for p in PROJ} for L in layers} for key, _, _, _ in ARMS}
|
| 73 |
+
|
| 74 |
+
# chart 2: output-change size on solid-colour vs general activations, layers 21-41 summed
|
| 75 |
+
glayers = [L for L in layers if "ef" in by[(L, "qkv")]["revert"]]
|
| 76 |
+
grows = [by[(L, p)] for L in glayers for p in PROJ]
|
| 77 |
+
ef0, eg0 = sum(r["revert"]["ef"] for r in grows), sum(r["revert"]["eg"] for r in grows)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
centred = all("efc" in r["revert"] for r in grows)
|
| 81 |
+
efc0, egc0 = (sum(r["revert"]["efc"] for r in grows), sum(r["revert"]["egc"] for r in grows)) if centred else (0, 0)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def point(key):
|
| 85 |
+
es = [edit(r, key) for r in grows]
|
| 86 |
+
p = {"x": ratio(sum(e["eg"] for e in es if e), eg0), "y": ratio(sum(e["ef"] for e in es if e), ef0)}
|
| 87 |
+
es = [edit(r, key, True) for r in grows]
|
| 88 |
+
if centred and all(e is None or "efc" in e for e in es):
|
| 89 |
+
p.update(xc=ratio(sum(e["egc"] for e in es if e), egc0), yc=ratio(sum(e["efc"] for e in es if e), efc0))
|
| 90 |
+
return p
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
rhos = sorted({float(k[len("solve_rho"):]) for k in grows[0] if k.startswith("solve_rho")})
|
| 94 |
+
sweep = []
|
| 95 |
+
for rho in rhos:
|
| 96 |
+
p = point(f"solve_rho{rho:g}")
|
| 97 |
+
p.update(rho=rho, w=ratio(sum(r[f"solve_rho{rho:g}"]["w2"] for r in grows), sum(r["revert"]["w2"] for r in grows)),
|
| 98 |
+
rendered=rendered(f"solve_mu0_rho{rho:g}_L21-41"))
|
| 99 |
+
sweep.append(p)
|
| 100 |
+
points = []
|
| 101 |
+
for key, label, slot, arm in ARMS:
|
| 102 |
+
p = point(key)
|
| 103 |
+
p.update(key=key, label=label, slot=slot, rendered=rendered(arm),
|
| 104 |
+
w=ratio(sum(edit(r, key)["w2"] for r in grows if edit(r, key)), sum(r["revert"]["w2"] for r in grows)))
|
| 105 |
+
points.append(p)
|
| 106 |
+
plain = rendered("swap055_L21-41")
|
| 107 |
+
|
| 108 |
+
# chart 3: per-layer output-change size on solids vs general prompts
|
| 109 |
+
sel = {}
|
| 110 |
+
for key in ("solve_rho0.1", "solve_rho1", "kijai_m1"):
|
| 111 |
+
sel[key] = {"solid": [], "gen": []}
|
| 112 |
+
for L in glayers:
|
| 113 |
+
rs = [by[(L, p)] for p in PROJ]
|
| 114 |
+
es = [edit(r, key) for r in rs]
|
| 115 |
+
f, g = ("efc", "egc") if centred else ("ef", "eg")
|
| 116 |
+
es = [edit(r, key, centred) for r in rs]
|
| 117 |
+
sel[key]["solid"].append(ratio(sum(e[f] for e in es if e), sum(r["revert"][f] for r in rs)))
|
| 118 |
+
sel[key]["gen"].append(ratio(sum(e[g] for e in es if e), sum(r["revert"][g] for r in rs)))
|
| 119 |
+
|
| 120 |
+
data = {"arms": [[k, l, s] for k, l, s, _ in ARMS], "layers": layers, "size": size, "per_matrix": per_matrix,
|
| 121 |
+
"sweep": sweep, "points": points, "plain": plain, "glayers": glayers, "sel": sel, "centred": centred}
|
| 122 |
+
html = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "anatomy_template.html")).read()
|
| 123 |
+
open(os.path.join(OUT, "edit_anatomy.html"), "w").write(html.replace("__DATA__", json.dumps(data, ensure_ascii=False)))
|
| 124 |
+
print("edit_anatomy.html written")
|
research/build_compare.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Rebuild output/moss/compare_solve.html from output/moss/drift/scores.jsonl (latest row per arm/item/seed).
|
| 3 |
+
|
| 4 |
+
python3 workflows/moss/build_compare.py [--public <dir> --arms a,b,c]
|
| 5 |
+
Arms listed in ARMS get a label (and an optional tag); arms that are scored but not listed appear under their folder name.
|
| 6 |
+
--public writes a self-contained copy for hosting: <dir>/index.html with only the given arms and WebP copies of their renders
|
| 7 |
+
(<dir>/img/<arm>/<item>__s<seed>.webp, quality 90; the colour view on lossy WebP is close to, not identical to, the PNGs).
|
| 8 |
+
"""
|
| 9 |
+
import argparse, datetime, glob, json, os, statistics
|
| 10 |
+
from PIL import Image
|
| 11 |
+
|
| 12 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 13 |
+
OUT = os.path.join(HERE, "..", "..", "output", "moss")
|
| 14 |
+
|
| 15 |
+
# (arm, label, tag) in menu order; the first is the default middle column
|
| 16 |
+
ARMS = [
|
| 17 |
+
("solve_mu0_rho0.1_L21-41", "Gram solve ρ=0.1, 21–41 (no detail protection)", ""),
|
| 18 |
+
("win_rho0.1_s13-25", "Gram solve ρ=0.1, steps 13–25 only", "new"),
|
| 19 |
+
("gcsolve_mu0.3_rho0.1_L21-41", "ρ=0.1 solve + protect general-prompt detail μ=0.3, 21–41", "new"),
|
| 20 |
+
("strong_full", "DeBand Strong @1.0 (full-rank LoRA, μ0.3 ρ0.05 solve)", "release"),
|
| 21 |
+
("gcsolve_mu0.3_rho0.05_x1.2_L21-41", "DeBand Strong @1.2 (same solve ×1.2)", "release"),
|
| 22 |
+
("strong_r512", "DeBand Strong compressed to rank 512", "compression test"),
|
| 23 |
+
("strong_r256", "DeBand Strong compressed to rank 256", "compression test"),
|
| 24 |
+
("strong_r128", "DeBand Strong compressed to rank 128", "compression test"),
|
| 25 |
+
("strong_r64", "DeBand Strong compressed to rank 64", "compression test"),
|
| 26 |
+
("sft_cfg25", "SFT at CFG 2.5, no SLG/TSR (reference for the *_cfg25 arms)", "reference"),
|
| 27 |
+
("nynxz_1.5_cfg25", "nynxz @1.5 at CFG 2.5, no SLG/TSR", "matched"),
|
| 28 |
+
("gcsolve_mu0.3_rho0.05_cfg25", "Our μ0.3 ρ0.05 solve at CFG 2.5, no SLG/TSR", "matched"),
|
| 29 |
+
("nynxz_1.0", "nynxz Destripe LoRA @1.0 (64 gradient-picked directions, 1.9 MB)", "external"),
|
| 30 |
+
("nynxz_1.5", "nynxz Destripe LoRA @1.5 (their 1024 px setting)", "external"),
|
| 31 |
+
("nynxz_1.5_gc03r005", "nynxz Destripe @1.5 stacked on μ0.3 ρ0.05 solve", "new"),
|
| 32 |
+
("dbA", "Trained de-band LoRA, 600 steps (arm A)", "overnight"),
|
| 33 |
+
("dbA_stale", "Arm A LoRA + μ0.3 repair fitted on plain SFT", "overnight"),
|
| 34 |
+
("dbB", "Arm A LoRA + μ0.3 repair refitted on A's activations (arm B)", "overnight"),
|
| 35 |
+
("dbC1", "Interleaved round 1: LoRA 200 steps", "overnight"),
|
| 36 |
+
("dbC1_rep", "Interleaved round 1 + refitted repair", "overnight"),
|
| 37 |
+
("dbC2", "Interleaved round 2 (repair + LoRA 400 steps)", "overnight"),
|
| 38 |
+
("dbC3", "Interleaved round 3 (repair + LoRA 600 steps)", "overnight"),
|
| 39 |
+
("dbC_final", "Interleaved final: LoRA 600 steps + last refitted repair (arm C)", "overnight"),
|
| 40 |
+
("gcsolve_mu0.3_rho0.05_L21-41", "ρ=0.05 solve + protect general detail μ=0.3, 21–41", "new"),
|
| 41 |
+
("gcsolve_mu0.3_rho0.1_x1.2_L21-41", "ρ=0.1 solve + protect detail μ=0.3, strength 1.2", "new"),
|
| 42 |
+
("gcsolve_mu0.5_rho0.1_L21-41", "ρ=0.1 solve + protect general detail μ=0.5, 21–41", "new"),
|
| 43 |
+
("gcsolve_mu1_rho0.1_L21-41", "ρ=0.1 solve + protect general-prompt detail μ=1, 21–41", "new"),
|
| 44 |
+
("gcsolve_mu3_rho0.1_L21-41", "ρ=0.1 solve + protect general-prompt detail μ=3, 21–41", "new"),
|
| 45 |
+
("dlsolve_rho0.1_L21-41", "ρ=0.1 solve, keep SFT on late-step detail, 21–41", "new"),
|
| 46 |
+
("dsolve_rho0.1_L21-41", "DC-only solve ρ=0.1 (Base on colour/brightness part only), 21–41", "new"),
|
| 47 |
+
("dsolve_rho0.1_x1.5_L21-41", "DC-only solve ρ=0.1 ×1.5, 21–41", "new"),
|
| 48 |
+
("dsolve_rho0.1_x2_L21-41", "DC-only solve ρ=0.1 ×2, 21–41", "new"),
|
| 49 |
+
("dsolve_rho0.001_L21-41", "DC-only solve ρ=0.001, 21–41", "new"),
|
| 50 |
+
("csolve_rho0.1_L21-41", "Centred-only solve ρ=0.1 (Base on horizontal variation only), 21–41", "dead end"),
|
| 51 |
+
("csolve_rho0.001_L21-41", "Centred-only solve ρ=0.001, 21–41", "dead end"),
|
| 52 |
+
("split_rho0.1_L32-41", "Gram solve ρ=0.1, layers 32–41 only", "new"),
|
| 53 |
+
("split_rho0.1_L21-31", "Gram solve ρ=0.1, layers 21–31 only", "new"),
|
| 54 |
+
("solve_mu0_rho0.3_L21-41", "Gram solve ρ=0.3, 21–41", ""),
|
| 55 |
+
("solve_mu0_rho1_L21-41", "Gram solve ρ=1, 21–41", ""),
|
| 56 |
+
("qualityplus_lora", "Quality+ (shipped 37–41 + 32–36)", "shipped"),
|
| 57 |
+
("deband_lora", "DeBand LoRA (shipped 37–41)", "shipped"),
|
| 58 |
+
("kijai_m1", "Kijai rank-512 LoRA @ −1 (layers 3, 4, 21–41)", ""),
|
| 59 |
+
("kijai_m05", "Kijai rank-512 LoRA @ −0.5 (layers 3, 4, 21–41)", "new"),
|
| 60 |
+
("swap055_L21-41", "Plain revert ×0.55 (exact Base−SFT blend), 21–41", "control"),
|
| 61 |
+
("solve_mu0_L21-41", "Exact revert 21–41", ""),
|
| 62 |
+
("solve_mu0_L32-41", "Exact revert 32–41", ""),
|
| 63 |
+
("base", "Base (MOSS-Image Base)", "reference"),
|
| 64 |
+
]
|
| 65 |
+
HIDE = ("sft", "snoise", "smp_", "rope_") # diagnostics, not fix candidates
|
| 66 |
+
|
| 67 |
+
rows = {}
|
| 68 |
+
for line in open(os.path.join(OUT, "drift", "scores.jsonl")):
|
| 69 |
+
r = json.loads(line)
|
| 70 |
+
rows[(r["arm"], r["id"], r["seed"])] = r
|
| 71 |
+
|
| 72 |
+
S = {}
|
| 73 |
+
for (arm, pid, seed), r in rows.items():
|
| 74 |
+
S.setdefault(arm, {})[f"{pid}__s{seed}"] = {k: r.get(k) for k in ("band", "ssim", "dino_dist", "arcface", "hf_db")}
|
| 75 |
+
S.setdefault("sft", {})[f"{pid}__s{seed}"] = {"band": r["band_ref"]}
|
| 76 |
+
|
| 77 |
+
ap = argparse.ArgumentParser()
|
| 78 |
+
ap.add_argument("--public")
|
| 79 |
+
ap.add_argument("--arms")
|
| 80 |
+
cli = ap.parse_args()
|
| 81 |
+
|
| 82 |
+
labels = {a: (l, t) for a, l, t in ARMS}
|
| 83 |
+
order = [a for a, _, _ in ARMS if a in S or os.path.isdir(os.path.join(OUT, "drift", a))] + sorted(a for a in S if a not in labels and not a.startswith(HIDE))
|
| 84 |
+
if cli.public:
|
| 85 |
+
keep = cli.arms.split(",")
|
| 86 |
+
order = [a for a in keep if a in order or a == "sft"]
|
| 87 |
+
S = {a: v for a, v in S.items() if a in keep or a == "sft"}
|
| 88 |
+
arms_js = [[a, labels.get(a, (a, ""))[0]] for a in order]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def mean(v):
|
| 92 |
+
v = [x for x in v if x is not None]
|
| 93 |
+
return statistics.fmean(v) if v else None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
board = []
|
| 97 |
+
for a in order:
|
| 98 |
+
rs = [r for (arm, _, _), r in rows.items() if arm == a]
|
| 99 |
+
solid = {c: mean([r.get("hf_db") for r in rs if r["id"] == "solid_" + c]) for c in ("grey", "blue", "beige")}
|
| 100 |
+
sv = [r["hf_db"] for r in rs if r.get("hf_db") is not None]
|
| 101 |
+
board.append(dict(arm=a, label=labels.get(a, (a, ""))[0], tag=labels.get(a, ("", ""))[1], **solid,
|
| 102 |
+
solid=mean(sv), solid_med=statistics.median(sv) if sv else None, solid_worst=max(sv) if sv else None,
|
| 103 |
+
arcface=mean([r.get("arcface") for r in rs if r["category"] == "people"]),
|
| 104 |
+
ssim=mean([r["ssim"] for r in rs if r["category"] != "solid"]),
|
| 105 |
+
dino=mean([r.get("dino_dist") for r in rs]),
|
| 106 |
+
band=mean([r["band"] for r in rs if r["category"] == "bench"]), n=len(rs)))
|
| 107 |
+
|
| 108 |
+
html = open(os.path.join(HERE, "compare_template.html")).read()
|
| 109 |
+
html = (html.replace("__S__", json.dumps(S)).replace("__ARMS__", json.dumps(arms_js, ensure_ascii=False))
|
| 110 |
+
.replace("__BOARD__", json.dumps(board, ensure_ascii=False)).replace("__WHEN__", datetime.datetime.now().strftime("%Y-%m-%d %H:%M")))
|
| 111 |
+
if cli.public:
|
| 112 |
+
for a in set(order) | {"sft", "base"}:
|
| 113 |
+
for f in glob.glob(os.path.join(OUT, "drift", a, "*_00001_.png")):
|
| 114 |
+
dst = os.path.join(cli.public, "img", a, os.path.basename(f).replace("_00001_.png", ".webp"))
|
| 115 |
+
if not os.path.exists(dst):
|
| 116 |
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
| 117 |
+
Image.open(f).convert("RGB").save(dst, quality=90, method=6)
|
| 118 |
+
html = html.replace("__IMG__", json.dumps("img/{arm}/{item}__s{seed}.webp"))
|
| 119 |
+
open(os.path.join(cli.public, "index.html"), "w").write(html)
|
| 120 |
+
print(f"{cli.public}/index.html: {len(order)} arms")
|
| 121 |
+
else:
|
| 122 |
+
html = html.replace("__IMG__", json.dumps("drift/{arm}/{item}__s{seed}_00001_.png"))
|
| 123 |
+
open(os.path.join(OUT, "compare_solve.html"), "w").write(html)
|
| 124 |
+
if not cli.public:
|
| 125 |
+
print(f"compare_solve.html: {len(order)} arms")
|
research/build_lowrank.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Truncated-SVD versions of a full-rank .diff banding-fix LoRA (ComfyUI lora_up/lora_down + alpha; norm diffs kept as-is).
|
| 3 |
+
|
| 4 |
+
CUDA_VISIBLE_DEVICES=0 comfyui_venv/bin/python workflows/moss/build_lowrank.py <src.safetensors> <rank> [<rank> ...]
|
| 5 |
+
Writes <src stem>_r<rank>.safetensors next to the source, in bf16.
|
| 6 |
+
"""
|
| 7 |
+
import os, sys
|
| 8 |
+
import torch
|
| 9 |
+
from safetensors import safe_open
|
| 10 |
+
from safetensors.torch import save_file
|
| 11 |
+
|
| 12 |
+
src, ranks = sys.argv[1], [int(r) for r in sys.argv[2:]]
|
| 13 |
+
with safe_open(src, "pt") as f:
|
| 14 |
+
diffs = {k: f.get_tensor(k) for k in f.keys()}
|
| 15 |
+
svd = {}
|
| 16 |
+
for k, d in diffs.items():
|
| 17 |
+
if d.dim() == 2:
|
| 18 |
+
U, S, Vh = torch.linalg.svd(d.cuda().float(), full_matrices=False)
|
| 19 |
+
svd[k] = (U, S, Vh)
|
| 20 |
+
for r in ranks:
|
| 21 |
+
out = {}
|
| 22 |
+
for k, d in diffs.items():
|
| 23 |
+
base = k[:-len(".diff")]
|
| 24 |
+
if d.dim() != 2:
|
| 25 |
+
out[k] = d
|
| 26 |
+
continue
|
| 27 |
+
U, S, Vh = svd[k]
|
| 28 |
+
root = S[:r].sqrt()
|
| 29 |
+
out[base + ".lora_up.weight"] = (U[:, :r] * root).to(torch.bfloat16).cpu().contiguous()
|
| 30 |
+
out[base + ".lora_down.weight"] = (root[:, None] * Vh[:r]).to(torch.bfloat16).cpu().contiguous()
|
| 31 |
+
out[base + ".alpha"] = torch.tensor(float(r))
|
| 32 |
+
path = os.path.splitext(src)[0] + f"_r{r}.safetensors"
|
| 33 |
+
save_file(out, path)
|
| 34 |
+
print(path, round(os.path.getsize(path) / 1e6, 1), "MB")
|
research/build_trainset.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Assemble a TrainLoraNode dataset folder in ComfyUI's input/ from rendered targets.
|
| 3 |
+
|
| 4 |
+
comfyui_venv/bin/python workflows/moss/build_trainset.py <name> --fix output/moss/trainset/fix_gc03 [--min-db 6] [--preserve gen]
|
| 5 |
+
fix: renders whose HF chroma power (bandviz.hf_chroma_power) is at least --min-db below the median of the plain-SFT capture renders
|
| 6 |
+
of the same prompt (output/moss/gram_capture/flat_*); the absolute level depends on the colour, so the reference is per prompt.
|
| 7 |
+
preserve: plain SFT renders of a Gram-capture prompt set (output/moss/gram_capture/<preserve>_*.png), kept as-is.
|
| 8 |
+
Captions are the prompts embedded in each PNG. Writes input/<name>/ and input/<name>/manifest.json.
|
| 9 |
+
"""
|
| 10 |
+
import argparse, glob, json, os, shutil, sys
|
| 11 |
+
import numpy as np
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
ROOT = os.path.join(HERE, "..", "..")
|
| 16 |
+
sys.path.insert(0, HERE)
|
| 17 |
+
import bandviz # noqa: E402
|
| 18 |
+
|
| 19 |
+
ap = argparse.ArgumentParser()
|
| 20 |
+
ap.add_argument("name")
|
| 21 |
+
ap.add_argument("--fix", required=True)
|
| 22 |
+
ap.add_argument("--min-db", type=float, default=6.0)
|
| 23 |
+
ap.add_argument("--preserve", default="gen")
|
| 24 |
+
a = ap.parse_args()
|
| 25 |
+
|
| 26 |
+
out = os.path.join(ROOT, "input", a.name)
|
| 27 |
+
os.makedirs(out, exist_ok=True)
|
| 28 |
+
manifest = {"fix": [], "rejected": [], "preserve": []}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def add(path, stem):
|
| 32 |
+
shutil.copy(path, os.path.join(out, stem + ".png"))
|
| 33 |
+
with open(os.path.join(out, stem + ".txt"), "w") as fh:
|
| 34 |
+
fh.write(prompt(path))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def prompt(path):
|
| 38 |
+
return json.loads(Image.open(path).info["prompt"])["6"]["inputs"]["value"]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
ref = {}
|
| 42 |
+
for f in glob.glob(os.path.join(ROOT, "output", "moss", "gram_capture", "flat_[0-9][0-9][0-9]_*_00001_.png")):
|
| 43 |
+
ref.setdefault(prompt(f), []).append(10 * np.log10(bandviz.hf_chroma_power(f)))
|
| 44 |
+
for f in sorted(glob.glob(os.path.join(ROOT, a.fix, "*_00001_.png"))):
|
| 45 |
+
db = 10 * np.log10(bandviz.hf_chroma_power(f)) - float(np.median(ref[prompt(f)]))
|
| 46 |
+
stem = "fix_" + os.path.basename(f).replace("_00001_.png", "")
|
| 47 |
+
if db <= -a.min_db:
|
| 48 |
+
add(f, stem)
|
| 49 |
+
manifest["fix"].append([stem, round(db, 1)])
|
| 50 |
+
else:
|
| 51 |
+
manifest["rejected"].append([stem, round(db, 1)])
|
| 52 |
+
for f in sorted(glob.glob(os.path.join(ROOT, "output", "moss", "gram_capture", f"{a.preserve}_[0-9][0-9][0-9]_*_00001_.png"))):
|
| 53 |
+
stem = "keep_" + os.path.basename(f).replace("_00001_.png", "")
|
| 54 |
+
add(f, stem)
|
| 55 |
+
manifest["preserve"].append(stem)
|
| 56 |
+
json.dump(manifest, open(os.path.join(out, "manifest.json"), "w"), indent=1)
|
| 57 |
+
print(f"{a.name}: {len(manifest['fix'])} fix (rejected {len(manifest['rejected'])}), {len(manifest['preserve'])} preserve")
|
research/compare_template.html
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
| 3 |
+
<title>MOSS DeBand Compare</title>
|
| 4 |
+
<style>
|
| 5 |
+
:root{--bg:#f6f6f4;--panel:#fff;--ink:#1d1d1f;--muted:#6b6b70;--line:#dedede;--accent:#2f6fde;--good:#2e7d4f;--bad:#b3402e;--hl:#eef3fd}
|
| 6 |
+
@media (prefers-color-scheme:dark){:root{--bg:#141416;--panel:#1d1d20;--ink:#ececee;--muted:#9a9aa0;--line:#33333a;--accent:#6aa0ff;--good:#6fcf97;--bad:#ff8a73;--hl:#1f2a3d}}
|
| 7 |
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,sans-serif}
|
| 8 |
+
header{padding:14px 16px 4px}h1{font-size:18px;margin:0 0 2px}.sub{color:var(--muted);font-size:13px}
|
| 9 |
+
.controls{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;padding:10px 16px}
|
| 10 |
+
select,button{font:inherit;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:4px 8px}
|
| 11 |
+
select{max-width:min(100%,340px)}
|
| 12 |
+
button.on{border-color:var(--accent);color:var(--accent)}
|
| 13 |
+
.grid{display:grid;grid-template-columns:repeat(var(--ncol,3),minmax(0,1fr));gap:10px;padding:0 16px 16px}
|
| 14 |
+
@media (max-width:900px){.grid{grid-template-columns:1fr}}
|
| 15 |
+
.col.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
|
| 16 |
+
.col .head{display:flex;gap:6px;align-items:center;margin:0 0 6px}.col .head select{flex:1;min-width:0;font-size:13px}.col .head button{padding:2px 8px}
|
| 17 |
+
a{color:var(--accent)}
|
| 18 |
+
.col{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px;min-width:0}
|
| 19 |
+
.col h2{font-size:14px;margin:0 0 6px}
|
| 20 |
+
.view{position:relative;width:100%;overflow:hidden;background:#000;border-radius:6px;cursor:grab;touch-action:none}
|
| 21 |
+
.view canvas{position:absolute;left:0;top:0;transform-origin:0 0;image-rendering:auto}
|
| 22 |
+
.view.drag{cursor:grabbing}
|
| 23 |
+
.stats{font-size:12px;color:var(--muted);margin-top:6px;display:flex;flex-wrap:wrap;gap:4px 12px}
|
| 24 |
+
.stats b{color:var(--ink)}.good{color:var(--good)!important}.bad{color:var(--bad)!important}
|
| 25 |
+
.items{display:flex;flex-wrap:wrap;gap:4px;padding:0 16px 8px}
|
| 26 |
+
.items button{font-size:12px;padding:2px 8px}
|
| 27 |
+
.note{color:var(--muted);font-size:12px;padding:0 16px 16px}
|
| 28 |
+
.board{padding:0 16px 8px;overflow-x:auto}
|
| 29 |
+
table{border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums;width:100%;background:var(--panel);border:1px solid var(--line);border-radius:10px}
|
| 30 |
+
th,td{padding:4px 8px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap}
|
| 31 |
+
th{color:var(--muted);font-weight:600;cursor:pointer;user-select:none}th:first-child,td:first-child{text-align:left}
|
| 32 |
+
tr.arm{cursor:pointer}tr.arm:hover,tr.sel{background:var(--hl)}
|
| 33 |
+
td.tag{color:var(--accent);font-size:11px}
|
| 34 |
+
.explain{margin:4px 16px 10px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px;font-size:13px;max-width:1100px}
|
| 35 |
+
.explain summary{cursor:pointer;font-weight:600}.explain p{margin:6px 0}
|
| 36 |
+
</style></head><body>
|
| 37 |
+
<header><h1>MOSS SFT DeBand: candidate fixes side by side</h1>
|
| 38 |
+
<div class="sub">Generated __WHEN__. Same prompt and seed in every column. Add, remove or change columns freely (your layout is kept in the page link). Drag to pan, wheel or buttons to zoom; all columns move together. Click a leaderboard row to put it in the selected column (outlined); shift-click adds it as a new column. <a href="edit_anatomy.html">What each fix changes inside the model →</a></div></header>
|
| 39 |
+
<details class="explain"><summary>What the arms are (exact revert vs Gram solve, what ρ means)</summary>
|
| 40 |
+
<p><b>Exact revert 21–41</b>: copy MOSS-Image Base's weights into SFT for transformer layers 21–41 (every attention and MLP matrix). It removes the stripes about as well as Base, but it also undoes SFT's fine-tuning in half the network, so faces and style drift a lot (ArcFace ≈ 0.65).</p>
|
| 41 |
+
<p><b>Gram solve</b>: instead of copying Base's weights, each layer gets a weight change D chosen so that its <i>output</i> matches Base only for the kinds of input the layer actually sees while SFT draws a solid colour. Those inputs were recorded from 36 solid-colour SFT renders (the "flat Gram" G). For inputs that look unlike those, the layer keeps SFT's behaviour. Formula: D = (W<sub>Base</sub> − W<sub>SFT</sub>) · G (G + εI)<sup>−1</sup>.</p>
|
| 42 |
+
<p><b>ρ</b> sets ε (ε = ρ × the average eigenvalue of G). It controls how narrow the edit is. ρ → 0 makes D the full Base−SFT difference, the same as the exact revert. Larger ρ keeps only the strongest solid-colour input directions, so the edit gets smaller and the result looks more like SFT: ρ=0.1 keeps most of the revert (cleanest solids, faces ≈ 0.76), ρ=1 keeps less (faces ≈ 0.81, solids only −5 dB), and ρ=10 barely changes anything.</p>
|
| 43 |
+
<p><b>Kijai LoRA @ −1 / −0.5</b>: subtracts Kijai's LoRA (or half of it). It is a rank-512 compressed approximation of the SFT↔Base difference covering layers 3, 4 and 21–41 plus the norms, so even at −1 it is only part of a full revert. It is not the same as our plain revert.</p>
|
| 44 |
+
<p><b>Plain revert ×0.55</b>: the control, our own exact blend W = SFT + 0.55·(Base − SFT) on layers 21–41, using the full difference with no compression. It moves every weight 55% of the way to Base, an edit about as big as ρ=0.1's. It only reaches −5.6 dB at about the same face drift, so the solve really is choosing <i>where</i> to edit, not just editing less.</p>
|
| 45 |
+
<p><b>Centred-only / DC-only solves</b>: the activations are split into two parts. One is each token row's average along the width: the overall colour and brightness, plus the timestep modulation shift ("DC"). The other is the horizontal variation around that average, which is where the stripes live. <i>Centred-only</i> makes the layers match Base on the horizontal variation and keeps SFT on the DC part. <i>DC-only</i> does the reverse. Centred-only does nothing for solids, DC-only gets about −5 dB with faces ≈ 0.86, and the full solve needs both parts together.</p>
|
| 46 |
+
<p><b>Protect general-prompt detail (μ)</b>: the ρ=0.1 solve plus a second term that keeps SFT's behaviour on the horizontal detail of 36 general prompts (faces, texture), with the colour/brightness part removed so it doesn't block the fix. Larger μ protects more. <b>Keep SFT on late-step detail</b>: Base everywhere the ρ=0.1 solve acts, except on horizontal variation in the last ~9 steps, where fine detail forms.</p>
|
| 47 |
+
<p><b>Steps 13–25 only</b>: the ρ=0.1 solve applied only during the last 13 of 25 sampling steps (a ComfyUI hook). <b>Split 21–31 / 32–41</b>: the ρ=0.1 solve on one half of the layers, to see which half fixes solids and which half moves faces. <b>Shipped</b> arms are the HF DeBand LoRA and Quality+ preset.</p>
|
| 48 |
+
</details>
|
| 49 |
+
<div class="board"><table id="board"></table></div>
|
| 50 |
+
<div class="note">Solids: HF colour power vs SFT in dB (orientation-free; lower = cleaner; Base ≈ −25). People: ArcFace identity vs SFT (higher = closer to SFT's face). SSIM / DINO: drift vs SFT on non-solid items. Banding: vertical-only fingerprint on the 768×1024 bench (1.0 = clean). Per-colour values are means over the seeds each arm has (11, and 14 for the main arms); "n" = images. Single images swing by 10–30 dB (one grey can hit −70), so sort by the median, not the mean.</div>
|
| 51 |
+
<div class="controls">
|
| 52 |
+
<button id="add">+ Add column</button>
|
| 53 |
+
<label>Seed <select id="seed"></select></label>
|
| 54 |
+
<span>Zoom <button data-z="0">Fit</button><button data-z="1">1:1</button><button data-z="2">2×</button><button data-z="4">4×</button></span>
|
| 55 |
+
<button id="cv">Color view (stripes) off</button>
|
| 56 |
+
<span class="sub">Keys: ←/→ item, 1–4 zoom, C color view</span>
|
| 57 |
+
</div>
|
| 58 |
+
<div class="items" id="items"></div>
|
| 59 |
+
<div class="grid" id="grid"></div>
|
| 60 |
+
<script>
|
| 61 |
+
const S = __S__;
|
| 62 |
+
const ARMS = __ARMS__;
|
| 63 |
+
const BOARD = __BOARD__;
|
| 64 |
+
const ITEMS = ["solid_grey","solid_blue","solid_beige","bench_fjord_sky","bench_fog_forest","p_elder_man_west_african","p_young_woman_east_asian","p_child_boy_black","a_anime_girl_school","x_golden_retriever","x_red_fox_snow","t_cafe_chalkboard","o_kitchen_scene"];
|
| 65 |
+
const $=id=>document.getElementById(id);
|
| 66 |
+
const CHOICES=[['base','Base (MOSS-Image Base)'],['sft','SFT (no fix)'],...ARMS.filter(a=>a[0]!=='base')];
|
| 67 |
+
const label=k=>(CHOICES.find(a=>a[0]===k)||[k,k])[1];
|
| 68 |
+
let item=ITEMS[0], seed=11, zoom=0, ox=0, oy=0, cv=false, W=1024, H=1024, active=1;
|
| 69 |
+
let colArms=['base',ARMS[0][0],'sft'];
|
| 70 |
+
try{const h=new URLSearchParams(location.hash.slice(1));if(h.get('cols'))colArms=h.get('cols').split(',').filter(Boolean);if(h.get('item')&&ITEMS.includes(h.get('item')))item=h.get('item');if(h.get('seed'))seed=+h.get('seed');}catch(e){}
|
| 71 |
+
let cols=[];
|
| 72 |
+
const seeds=[...new Set(Object.values(S).flatMap(o=>Object.keys(o).map(k=>+k.split('__s')[1])))].sort((a,b)=>a-b);
|
| 73 |
+
for(const s of seeds) $('seed').add(new Option(s,s));
|
| 74 |
+
$('items').innerHTML=ITEMS.map(i=>`<button data-i="${i}">${i.replace(/^(solid_|bench_|p_|a_|x_|t_|o_)/,'')}</button>`).join('');
|
| 75 |
+
document.querySelectorAll('#items button').forEach(b=>b.onclick=()=>{item=b.dataset.i;load();});
|
| 76 |
+
function saveHash(){try{history.replaceState(null,'','#cols='+colArms.join(',')+'&item='+item+'&seed='+seed);}catch(e){}}
|
| 77 |
+
function build(){const g=$('grid');g.innerHTML='';g.style.setProperty('--ncol',colArms.length);if(active>=colArms.length)active=colArms.length-1;
|
| 78 |
+
cols=colArms.map((a,i)=>{const d=document.createElement('div');d.className='col'+(i===active?' active':'');
|
| 79 |
+
const hd=document.createElement('div');hd.className='head';const sel=document.createElement('select');for(const [k,l] of CHOICES)sel.add(new Option(l,k));sel.value=a;
|
| 80 |
+
sel.onchange=()=>{colArms[i]=sel.value;active=i;load();};const rm=document.createElement('button');rm.textContent='✕';rm.title='Remove column';rm.disabled=colArms.length<=1;
|
| 81 |
+
rm.onclick=e=>{e.stopPropagation();colArms.splice(i,1);if(active>=i&&active>0)active--;build();load();};hd.append(sel,rm);
|
| 82 |
+
const v=document.createElement('div');v.className='view';const c=document.createElement('canvas');v.appendChild(c);const st=document.createElement('div');st.className='stats';
|
| 83 |
+
d.append(hd,v,st);d.addEventListener('pointerdown',()=>{active=i;document.querySelectorAll('.col').forEach((x,j)=>x.classList.toggle('active',j===i));markBoard();});g.appendChild(d);
|
| 84 |
+
const col={view:v,cvs:c,img:new Image(),stats:st,sel};attachPan(col);return col;});}
|
| 85 |
+
const IMG = __IMG__;
|
| 86 |
+
function src(a){return IMG.replace('{arm}',a).replace('{item}',item).replace('{seed}',seed);}
|
| 87 |
+
function colorize(ctx,w,h){const d=ctx.getImageData(0,0,w,h),p=d.data;for(let i=0;i<p.length;i+=4){const m=(p[i]+p[i+1]+p[i+2])/3;p[i]=Math.max(0,Math.min(255,(p[i]-m)*8+128));p[i+1]=Math.max(0,Math.min(255,(p[i+1]-m)*8+128));p[i+2]=Math.max(0,Math.min(255,(p[i+2]-m)*8+128));}ctx.putImageData(d,0,0);}
|
| 88 |
+
function draw(c){const ctx=c.cvs.getContext('2d',{willReadFrequently:true});if(!c.img.complete||!c.img.naturalWidth){c.cvs.width=W;c.cvs.height=H;ctx.fillStyle='#333';ctx.fillRect(0,0,W,H);ctx.fillStyle='#aaa';ctx.font='40px system-ui';ctx.fillText('not rendered for this item/seed',40,80);c.view.style.aspectRatio=W+'/'+H;render();return;}W=c.img.naturalWidth;H=c.img.naturalHeight;c.cvs.width=W;c.cvs.height=H;ctx.drawImage(c.img,0,0);if(cv)colorize(ctx,W,H);c.view.style.aspectRatio=W+'/'+H;render();}
|
| 89 |
+
function load(){cols.forEach((c,i)=>{c.sel.value=colArms[i];c.img=new Image();c.img.onload=()=>draw(c);c.img.onerror=()=>draw(c);c.img.src=src(colArms[i]);});stats();
|
| 90 |
+
document.querySelectorAll('#items button').forEach(b=>b.classList.toggle('on',b.dataset.i===item));$('seed').value=seed;document.querySelectorAll('.col').forEach((x,j)=>x.classList.toggle('active',j===active));markBoard();saveHash();}
|
| 91 |
+
function markBoard(){document.querySelectorAll('tr.arm').forEach(r=>r.classList.toggle('sel',colArms.includes(r.dataset.k)));}
|
| 92 |
+
function scale(){const vw=cols[0].view.clientWidth;return zoom===0?vw/W:zoom;}
|
| 93 |
+
function render(){if(!cols.length)return;const s=scale(),vw=cols[0].view.clientWidth,vh=vw*H/W;ox=Math.min(0,Math.max(vw-W*s,ox));oy=Math.min(0,Math.max(vh-H*s,oy));for(const c of cols){c.cvs.style.transform=`translate(${ox}px,${oy}px) scale(${s})`;}document.querySelectorAll('[data-z]').forEach(b=>b.classList.toggle('on',+b.dataset.z===zoom));}
|
| 94 |
+
function fmt(v,d=2){return v==null?'–':v.toFixed(d)}
|
| 95 |
+
function stats(){const key=`${item}__s${seed}`;
|
| 96 |
+
cols.forEach((c,i)=>{const a=colArms[i];const r=(S[a]||{})[key]||{};const b=r.band;const cls=b==null?'':(b<=1.25?'good':(b>=1.6?'bad':''));
|
| 97 |
+
let h=`<span>banding <b class="${cls}">${fmt(b)}</b></span>`;
|
| 98 |
+
if(a!=='sft'){if(r.hf_db!=null)h+=`<span>HF colour <b class="${r.hf_db<=-10?'good':(r.hf_db>-3?'bad':'')}">${r.hf_db>0?'+':''}${fmt(r.hf_db,1)} dB</b></span>`;
|
| 99 |
+
if(r.ssim!=null)h+=`<span>SSIM vs SFT <b>${fmt(r.ssim,3)}</b></span>`;if(r.dino_dist!=null)h+=`<span>DINO dist <b>${fmt(r.dino_dist,3)}</b></span>`;if(r.arcface!=null)h+=`<span>ArcFace <b class="${r.arcface<0.75?'bad':'good'}">${fmt(r.arcface,3)}</b></span>`;}
|
| 100 |
+
c.stats.innerHTML=h;});}
|
| 101 |
+
const COLS=[['label','Arm'],['grey','grey dB'],['blue','blue dB'],['beige','beige dB'],['solid_med','solid median dB'],['solid','solid mean dB'],['solid_worst','worst solid'],['arcface','ArcFace'],['ssim','SSIM'],['dino','DINO dist'],['band','bench banding'],['n','n']];
|
| 102 |
+
let sortKey='solid_med', sortDir=1;
|
| 103 |
+
function board(){const rows=[...BOARD].sort((a,b)=>{const x=a[sortKey],y=b[sortKey];if(x==null)return 1;if(y==null)return -1;return typeof x==='string'?x.localeCompare(y)*sortDir:(x-y)*sortDir;});
|
| 104 |
+
const cell=(r,k)=>{const v=r[k];if(v==null)return '<td>–</td>';if(k==='label')return `<td>${v}${r.tag?` <span class="tag">${r.tag}</span>`:''}</td>`;
|
| 105 |
+
if(['grey','blue','beige','solid','solid_med','solid_worst'].includes(k))return `<td class="${v<=-10?'good':(v>-3?'bad':'')}">${v>0?'+':''}${v.toFixed(1)}</td>`;
|
| 106 |
+
if(k==='arcface')return `<td class="${v<0.75?'bad':(v>=0.85?'good':'')}">${v.toFixed(3)}</td>`;if(k==='n')return `<td>${v}</td>`;return `<td>${v.toFixed(k==='band'?2:3)}</td>`;};
|
| 107 |
+
$('board').innerHTML='<tr>'+COLS.map(([k,l])=>`<th data-k="${k}">${l}${k===sortKey?(sortDir>0?' ▲':' ▼'):''}</th>`).join('')+'</tr>'+rows.map(r=>`<tr class="arm" data-k="${r.arm}">`+COLS.map(([k])=>cell(r,k)).join('')+'</tr>').join('');
|
| 108 |
+
document.querySelectorAll('#board th').forEach(th=>th.onclick=()=>{const k=th.dataset.k;sortDir=k===sortKey?-sortDir:(['arcface','ssim'].includes(k)?-1:1);sortKey=k;board();markBoard();});
|
| 109 |
+
document.querySelectorAll('tr.arm').forEach(tr=>tr.onclick=e=>{if(e.shiftKey){colArms.push(tr.dataset.k);active=colArms.length-1;build();}else colArms[active]=tr.dataset.k;load();});}
|
| 110 |
+
$('add').onclick=()=>{colArms.splice(colArms.length-(colArms[colArms.length-1]==='sft'?1:0),0,colArms[active]||ARMS[0][0]);active=colArms.indexOf('sft')>0?colArms.length-2:colArms.length-1;build();load();};
|
| 111 |
+
$('seed').onchange=e=>{seed=+e.target.value;load();};
|
| 112 |
+
document.querySelectorAll('[data-z]').forEach(b=>b.onclick=()=>{zoom=+b.dataset.z;render();});
|
| 113 |
+
$('cv').onclick=()=>{cv=!cv;$('cv').textContent='Color view (stripes) '+(cv?'on':'off');$('cv').classList.toggle('on',cv);cols.forEach(draw);};
|
| 114 |
+
let drag=null;
|
| 115 |
+
function attachPan(c){c.view.addEventListener('pointerdown',e=>{drag={x:e.clientX,y:e.clientY,ox,oy};c.view.setPointerCapture(e.pointerId);c.view.classList.add('drag');});
|
| 116 |
+
c.view.addEventListener('pointermove',e=>{if(!drag)return;ox=drag.ox+e.clientX-drag.x;oy=drag.oy+e.clientY-drag.y;render();});
|
| 117 |
+
c.view.addEventListener('pointerup',()=>{drag=null;c.view.classList.remove('drag');});
|
| 118 |
+
c.view.addEventListener('wheel',e=>{e.preventDefault();const r=c.view.getBoundingClientRect(),cx=e.clientX-r.left,cy=e.clientY-r.top,s0=scale();const ix=(cx-ox)/s0,iy=(cy-oy)/s0;const ns=Math.min(8,Math.max(cols[0].view.clientWidth/W,s0*(e.deltaY<0?1.25:0.8)));zoom=ns;ox=cx-ix*ns;oy=cy-iy*ns;render();},{passive:false});}
|
| 119 |
+
document.addEventListener('keydown',e=>{if(e.target.tagName==='SELECT')return;const i=ITEMS.indexOf(item);
|
| 120 |
+
if(e.key==='ArrowRight'){item=ITEMS[(i+1)%ITEMS.length];load();}else if(e.key==='ArrowLeft'){item=ITEMS[(i+ITEMS.length-1)%ITEMS.length];load();}
|
| 121 |
+
else if('1234'.includes(e.key)){zoom=[0,1,2,4][+e.key-1];render();}else if(e.key==='c'||e.key==='C')$('cv').click();});
|
| 122 |
+
window.addEventListener('resize',render);board();build();load();
|
| 123 |
+
</script></body></html>
|
research/drift_runner.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Queue one arm of the DeBand screen through the ComfyUI API: drift suite (1024x1024) + banding bench (768x1024) + solid colours.
|
| 3 |
+
|
| 4 |
+
python3 workflows/moss/drift_runner.py <arm> [model options] [sampler options] [--full | --only id,id] [--front]
|
| 5 |
+
Model options (chained in this order on the SFT UNet): --swap ratio,layers,parts (MOSSBlockSwap toward Base),
|
| 6 |
+
--solve mu,rho,layers[,strength] (MOSSGramSolve), --rope col_scale,row_scale,image_shift,common_shift (MOSSRopeOverride),
|
| 7 |
+
--lora file:strength (first LoRA slot), --unet file, --preset N.
|
| 8 |
+
Sampler options: --cfg, --no-slg, --no-tsr, --euler, --s-noise.
|
| 9 |
+
Renders go to output/moss/drift/<arm>/<id>__s<seed>_00001_.png.
|
| 10 |
+
"""
|
| 11 |
+
import argparse, copy, json, os, urllib.request
|
| 12 |
+
|
| 13 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
ROOT = os.path.join(HERE, "..", "..")
|
| 15 |
+
SOLID = "A completely flat, uniform, solid {c} background that fills the entire frame. Nothing else: no objects, no texture, no pattern, no gradient, no vignette, no text. An evenly lit seamless studio backdrop of one single color."
|
| 16 |
+
SCREEN = {"solid_grey", "solid_blue", "solid_beige", "bench_fog_forest", "bench_fjord_sky", "p_elder_man_west_african", "p_young_woman_east_asian",
|
| 17 |
+
"p_child_boy_black", "a_anime_girl_school", "x_golden_retriever", "x_red_fox_snow", "t_cafe_chalkboard", "o_kitchen_scene"}
|
| 18 |
+
|
| 19 |
+
ap = argparse.ArgumentParser()
|
| 20 |
+
ap.add_argument("arm")
|
| 21 |
+
ap.add_argument("--swap", help="ratio,layers,parts for MOSSBlockSwap toward Base (parts like qkv;o;ffn_up_gate;ffn_down or all)")
|
| 22 |
+
ap.add_argument("--solve", action="append", default=[], help="mu,rho,layers[,strength[,flat_tag[,num_tag or !num_tag for its complement[,gen_tag]]]] for MOSSGramSolve (layers like 21-41); repeat to chain layer groups")
|
| 23 |
+
ap.add_argument("--rope", help="col_scale,row_scale,image_shift,common_shift for MOSSRopeOverride")
|
| 24 |
+
ap.add_argument("--lora", help="file:strength replacing the first LoRA slot")
|
| 25 |
+
ap.add_argument("--window", help="file:strength:lo-hi: apply a LoRA as a hook only for sampling steps lo..hi (1-indexed, of 25)")
|
| 26 |
+
ap.add_argument("--unet", default="moss_image_sft_bf16.safetensors")
|
| 27 |
+
ap.add_argument("--preset", type=int, default=5, help="preset index of the stock workflow (5 = Off)")
|
| 28 |
+
ap.add_argument("--cfg", type=float)
|
| 29 |
+
ap.add_argument("--no-slg", action="store_true")
|
| 30 |
+
ap.add_argument("--no-tsr", action="store_true")
|
| 31 |
+
ap.add_argument("--euler", action="store_true", help="Euler instead of ER-SDE")
|
| 32 |
+
ap.add_argument("--s-noise", type=float)
|
| 33 |
+
ap.add_argument("--seeds", default="11")
|
| 34 |
+
ap.add_argument("--full", action="store_true", help="all 32 items instead of the 13-item screen set")
|
| 35 |
+
ap.add_argument("--only", help="comma list of item ids (or extra solid/size items like solid_grey@768x1360)")
|
| 36 |
+
ap.add_argument("--front", action="store_true", help="put the jobs at the front of the ComfyUI queue")
|
| 37 |
+
ap.add_argument("--url", default="http://localhost:8188")
|
| 38 |
+
a = ap.parse_args()
|
| 39 |
+
|
| 40 |
+
wf = json.load(open(os.path.join(HERE, "moss_banding_fix_presets_api.json")))
|
| 41 |
+
api, names = wf["prompt"], wf["presets"]
|
| 42 |
+
items = [(d["id"], d["prompt"], 1024, 1024) for d in json.load(open(os.path.join(HERE, "bench/drift_suite_v1.json")))]
|
| 43 |
+
items += [("bench_" + d["id"], d["prompt"], 768, 1024) for d in json.load(open(os.path.join(HERE, "bench/prompts_v1.json")))
|
| 44 |
+
if d["id"] in ("fog_forest", "fjord_sky", "goose", "oil_still_life", "product_glass")]
|
| 45 |
+
colors = {"grey": "neutral mid grey", "blue": "soft sky blue", "beige": "warm beige"}
|
| 46 |
+
items += [("solid_" + k, SOLID.format(c=c), 1024, 1024) for k, c in colors.items()]
|
| 47 |
+
if a.only:
|
| 48 |
+
by_id = {i[0]: i for i in items}
|
| 49 |
+
picked = []
|
| 50 |
+
for tok in a.only.split(","):
|
| 51 |
+
if "@" in tok: # e.g. solid_grey@768x1360
|
| 52 |
+
base, size = tok.split("@")
|
| 53 |
+
w, h = (int(v) for v in size.split("x"))
|
| 54 |
+
picked.append((f"{base}_{w}x{h}", by_id[base][1], w, h))
|
| 55 |
+
else:
|
| 56 |
+
picked.append(by_id[tok])
|
| 57 |
+
items = picked
|
| 58 |
+
elif not a.full:
|
| 59 |
+
items = [i for i in items if i[0] in SCREEN]
|
| 60 |
+
|
| 61 |
+
n = 0
|
| 62 |
+
for seed in (int(s) for s in a.seeds.split(",")):
|
| 63 |
+
for pid, text, w, h in items:
|
| 64 |
+
p = copy.deepcopy(api)
|
| 65 |
+
p["1"]["inputs"].update(choice=names[a.preset], index=a.preset)
|
| 66 |
+
p["3"]["inputs"]["unet_name"] = a.unet
|
| 67 |
+
p["6"]["inputs"]["value"] = text
|
| 68 |
+
p["22"]["inputs"]["value"] = seed
|
| 69 |
+
p["23"]["inputs"].update(width=w, height=h)
|
| 70 |
+
cur = ["3", 0]
|
| 71 |
+
if a.swap:
|
| 72 |
+
ratio, layers, parts = a.swap.split(",")
|
| 73 |
+
p["95"] = {"class_type": "UNETLoader", "inputs": {"unet_name": "moss_image_base_bf16.safetensors", "weight_dtype": "default"}}
|
| 74 |
+
p["97"] = {"class_type": "MOSSBlockSwap", "inputs": {"model": cur, "donor": ["95", 0], "layers": layers,
|
| 75 |
+
"parts": parts.replace(";", ","), "include_other": False, "ratio": float(ratio)}}
|
| 76 |
+
cur = ["97", 0]
|
| 77 |
+
for j, spec in enumerate(a.solve):
|
| 78 |
+
v = spec.split(",")
|
| 79 |
+
p[str(90 + j * 100)] = {"class_type": "MOSSGramSolve", "inputs": {
|
| 80 |
+
"model": cur, "sft_file": "moss_image_sft_bf16.safetensors", "base_file": "moss_image_base_bf16.safetensors",
|
| 81 |
+
"flat_tag": v[4] if len(v) > 4 and v[4] else "flat", "num_tag": v[5].lstrip("!") if len(v) > 5 else "", "num_complement": len(v) > 5 and v[5].startswith("!"), "gen_tag": v[6] if len(v) > 6 else "gen", "layers": v[2], "parts": "qkv,o,ffn_up_gate,ffn_down",
|
| 82 |
+
"mu": float(v[0]), "rho": float(v[1]), "strength": float(v[3]) if len(v) > 3 else 1.0, "device": "cuda:0"}}
|
| 83 |
+
cur = [str(90 + j * 100), 0]
|
| 84 |
+
if a.rope:
|
| 85 |
+
c, r, s, cs = (float(x) for x in a.rope.split(","))
|
| 86 |
+
p["96"] = {"class_type": "MOSSRopeOverride", "inputs": {"model": cur, "col_scale": c, "row_scale": r, "image_shift": s, "common_shift": cs}}
|
| 87 |
+
cur = ["96", 0]
|
| 88 |
+
p["14"]["inputs"]["model"] = cur
|
| 89 |
+
if a.lora:
|
| 90 |
+
f, s = a.lora.rsplit(":", 1)
|
| 91 |
+
p["14"]["inputs"].update(lora_name=f, strength_model=float(s))
|
| 92 |
+
# sampling chain: LoRAs (17) -> TSR (18) -> SLG (19) -> scheduler (20) / SamplerCustom (24)
|
| 93 |
+
sampling_model = ["19", 0]
|
| 94 |
+
if a.no_slg:
|
| 95 |
+
del p["19"]
|
| 96 |
+
sampling_model = ["18", 0]
|
| 97 |
+
if a.no_tsr:
|
| 98 |
+
if a.no_slg:
|
| 99 |
+
sampling_model = ["17", 0]
|
| 100 |
+
else:
|
| 101 |
+
p["19"]["inputs"]["model"] = ["17", 0]
|
| 102 |
+
del p["18"]
|
| 103 |
+
p["20"]["inputs"]["model"] = sampling_model
|
| 104 |
+
p["24"]["inputs"]["model"] = sampling_model
|
| 105 |
+
if a.cfg is not None:
|
| 106 |
+
p["24"]["inputs"]["cfg"] = a.cfg
|
| 107 |
+
if a.euler:
|
| 108 |
+
p["21"] = {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler"}}
|
| 109 |
+
elif a.s_noise is not None:
|
| 110 |
+
p["21"]["inputs"]["s_noise"] = a.s_noise
|
| 111 |
+
if a.window:
|
| 112 |
+
f, s, rng = a.window.rsplit(":", 2)
|
| 113 |
+
lo, hi = (int(v) for v in rng.split("-"))
|
| 114 |
+
p["70"] = {"class_type": "CreateHookLoraModelOnly", "inputs": {"lora_name": f, "strength_model": float(s)}}
|
| 115 |
+
kf = ([(0.0, 0.0)] if lo > 1 else []) + [((lo - 1) / 25, 1.0)] + ([(hi / 25, 0.0)] if hi < 25 else [])
|
| 116 |
+
prev = None
|
| 117 |
+
for i, (pct, mult) in enumerate(kf):
|
| 118 |
+
p[str(71 + i)] = {"class_type": "CreateHookKeyframe", "inputs": {"strength_mult": mult, "start_percent": pct}}
|
| 119 |
+
if prev:
|
| 120 |
+
p[str(71 + i)]["inputs"]["prev_hook_kf"] = [prev, 0]
|
| 121 |
+
prev = str(71 + i)
|
| 122 |
+
p["75"] = {"class_type": "SetHookKeyframes", "inputs": {"hooks": ["70", 0], "hook_kf": [prev, 0]}}
|
| 123 |
+
for cid, src in (("76", "7"), ("77", "8")):
|
| 124 |
+
p[cid] = {"class_type": "ConditioningSetProperties", "inputs": {"cond_NEW": [src, 0], "strength": 1.0, "set_cond_area": "default", "hooks": ["75", 0]}}
|
| 125 |
+
p["24"]["inputs"]["positive"], p["24"]["inputs"]["negative"] = ["76", 0], ["77", 0]
|
| 126 |
+
p["26"]["inputs"]["filename_prefix"] = f"moss/drift/{a.arm}/{pid}__s{seed}"
|
| 127 |
+
r = json.load(urllib.request.urlopen(urllib.request.Request(a.url + "/prompt", json.dumps({"prompt": p, "front": a.front}).encode(), {"Content-Type": "application/json"})))
|
| 128 |
+
assert not r.get("node_errors"), r
|
| 129 |
+
n += 1
|
| 130 |
+
print(f"{a.arm}: queued {n}")
|
research/drift_score.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Score DeBand arms against a reference arm (paired by prompt id and seed) from output/moss/drift/<arm>/.
|
| 3 |
+
|
| 4 |
+
comfyui_venv/bin/python workflows/moss/drift_score.py <ref_arm> <arm> [<arm> ...]
|
| 5 |
+
Per image: banding (bandviz.local_score), SSIM to ref, DINOv2-base cosine distance to ref, and for photo people the ArcFace
|
| 6 |
+
cosine of the largest face (buffalo_l; a missing face is reported as missing, not as 0). Prints per-category mean and the worst
|
| 7 |
+
prompt, and writes output/moss/drift/scores.jsonl (one row per arm/image/metric).
|
| 8 |
+
"""
|
| 9 |
+
import glob, json, os, re, sys
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from PIL import Image
|
| 13 |
+
from skimage.metrics import structural_similarity as ssim
|
| 14 |
+
from transformers import AutoImageProcessor, AutoModel
|
| 15 |
+
from insightface.app import FaceAnalysis
|
| 16 |
+
|
| 17 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 18 |
+
sys.path.insert(0, HERE)
|
| 19 |
+
import bandviz # noqa: E402
|
| 20 |
+
|
| 21 |
+
D = os.path.join(HERE, "..", "..", "output", "moss", "drift")
|
| 22 |
+
CATS = {d["id"]: d["category"] for d in json.load(open(os.path.join(HERE, "bench", "drift_suite_v1.json")))}
|
| 23 |
+
ref, arms = sys.argv[1], sys.argv[2:]
|
| 24 |
+
|
| 25 |
+
proc = AutoImageProcessor.from_pretrained("facebook/dinov2-base")
|
| 26 |
+
dino = AutoModel.from_pretrained("facebook/dinov2-base").eval()
|
| 27 |
+
face = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"])
|
| 28 |
+
face.prepare(ctx_id=-1, det_size=(640, 640))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def files(arm):
|
| 32 |
+
out = {}
|
| 33 |
+
for f in glob.glob(os.path.join(D, arm, "*_00001_.png")):
|
| 34 |
+
m = re.match(r"(.+)__s(\d+)_00001_\.png", os.path.basename(f))
|
| 35 |
+
out[(m.group(1), int(m.group(2)))] = f
|
| 36 |
+
return out
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def emb_dino(path):
|
| 40 |
+
with torch.inference_mode():
|
| 41 |
+
x = proc(images=Image.open(path).convert("RGB"), return_tensors="pt")
|
| 42 |
+
v = dino(**x).last_hidden_state[:, 0]
|
| 43 |
+
return torch.nn.functional.normalize(v, dim=-1)[0]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def emb_face(path):
|
| 47 |
+
faces = face.get(np.asarray(Image.open(path).convert("RGB"))[:, :, ::-1])
|
| 48 |
+
if not faces:
|
| 49 |
+
return None
|
| 50 |
+
f = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
|
| 51 |
+
return f.normed_embedding
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def category(pid):
|
| 55 |
+
return "bench" if pid.startswith("bench_") else "solid" if pid.startswith("solid_") else CATS.get(pid, "other")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
R = files(ref)
|
| 59 |
+
cache = {}
|
| 60 |
+
rows = []
|
| 61 |
+
for arm in arms:
|
| 62 |
+
A = files(arm)
|
| 63 |
+
for key in sorted(set(R) & set(A)):
|
| 64 |
+
pid, seed = key
|
| 65 |
+
fr, fa = R[key], A[key]
|
| 66 |
+
cat = category(pid)
|
| 67 |
+
row = dict(arm=arm, id=pid, seed=seed, category=cat, band=bandviz.local_score(fa), band_ref=bandviz.local_score(fr))
|
| 68 |
+
ar, aa = np.asarray(Image.open(fr).convert("RGB")), np.asarray(Image.open(fa).convert("RGB"))
|
| 69 |
+
row["ssim"] = float(ssim(ar, aa, channel_axis=2, data_range=255))
|
| 70 |
+
if cat == "solid":
|
| 71 |
+
row["hf_db"] = 10 * np.log10(max(bandviz.hf_chroma_power(fa), 1.0) / bandviz.hf_chroma_power(fr)) # a perfectly uniform render has zero power
|
| 72 |
+
if cat not in ("solid",):
|
| 73 |
+
if fr not in cache:
|
| 74 |
+
cache[fr] = emb_dino(fr)
|
| 75 |
+
row["dino_dist"] = float(1 - (cache[fr] @ emb_dino(fa)))
|
| 76 |
+
if cat == "people":
|
| 77 |
+
er, ea = emb_face(fr), emb_face(fa)
|
| 78 |
+
row["arcface"] = None if er is None or ea is None else float(np.dot(er, ea))
|
| 79 |
+
rows.append(row)
|
| 80 |
+
with open(os.path.join(D, "scores.jsonl"), "a") as fh:
|
| 81 |
+
for r in rows:
|
| 82 |
+
fh.write(json.dumps(r) + "\n")
|
| 83 |
+
|
| 84 |
+
for arm in arms:
|
| 85 |
+
rs = [r for r in rows if r["arm"] == arm]
|
| 86 |
+
print(f"\n=== {arm} vs {ref} ({len(rs)} paired images)")
|
| 87 |
+
for cat in ("solid", "bench", "people", "anime_people", "animals", "typography", "objects", "scenes"):
|
| 88 |
+
cr = [r for r in rs if r["category"] == cat]
|
| 89 |
+
if not cr:
|
| 90 |
+
continue
|
| 91 |
+
line = f" {cat:13s} n={len(cr):2d} banding {np.mean([r['band'] for r in cr]):.2f} (ref {np.mean([r['band_ref'] for r in cr]):.2f}) SSIM {np.mean([r['ssim'] for r in cr]):.3f}"
|
| 92 |
+
if cat == "solid":
|
| 93 |
+
line += f" HF colour power vs SFT {np.mean([r['hf_db'] for r in cr]):+.1f} dB (worst {max(r['hf_db'] for r in cr):+.1f})"
|
| 94 |
+
if "dino_dist" in cr[0]:
|
| 95 |
+
worst = max(cr, key=lambda r: r["dino_dist"])
|
| 96 |
+
line += f" DINO dist {np.mean([r['dino_dist'] for r in cr]):.3f} (worst {worst['dino_dist']:.3f} {worst['id']})"
|
| 97 |
+
if cat == "people":
|
| 98 |
+
af = [r["arcface"] for r in cr if r["arcface"] is not None]
|
| 99 |
+
line += f" ArcFace {np.mean(af):.3f} (min {min(af):.3f}, missing {len(cr) - len(af)})" if af else " ArcFace n/a"
|
| 100 |
+
print(line)
|
research/edit_anatomy.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Where each DeBand fix edits SFT, and how much it changes each layer's output on solid-colour vs general activations.
|
| 3 |
+
|
| 4 |
+
comfyui_venv/bin/python workflows/moss/edit_anatomy.py (CPU; writes output/moss/edit_anatomy.json)
|
| 5 |
+
Per layer and linear (qkv, o, ffn_up_gate, ffn_down), with Delta = W_base - W_sft and D a fix's weight change:
|
| 6 |
+
w2 = |D|^2 (weight-change energy; Delta's own is the full revert)
|
| 7 |
+
dot = <D, Delta> (how much of D points toward Base)
|
| 8 |
+
ef = tr(D G_f D^T) (output-change energy on the solid-colour activations, flat Gram, layers 21-41 only)
|
| 9 |
+
eg = tr(D G_g D^T) (same on the general-prompt activations, gen Gram)
|
| 10 |
+
fp32. Gram solves for any rho come from one eigendecomposition of G_f: D = Delta U diag(l / (l + eps)) U^T, eps = rho tr(G_f) / d.
|
| 11 |
+
"""
|
| 12 |
+
import json, os
|
| 13 |
+
import torch
|
| 14 |
+
from safetensors import safe_open
|
| 15 |
+
|
| 16 |
+
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
|
| 17 |
+
OUT = os.path.join(ROOT, "output", "moss", "edit_anatomy.json")
|
| 18 |
+
PROJ = ("qkv", "o", "ffn_up_gate", "ffn_down")
|
| 19 |
+
RHOS = (0.01, 0.03, 0.1, 0.3, 1.0, 3.0, 10.0)
|
| 20 |
+
torch.set_num_threads(16)
|
| 21 |
+
|
| 22 |
+
sft = safe_open(os.path.join(ROOT, "models/diffusion_models/moss_image_sft_bf16.safetensors"), "pt")
|
| 23 |
+
base = safe_open(os.path.join(ROOT, "models/diffusion_models/moss_image_base_bf16.safetensors"), "pt")
|
| 24 |
+
gf = safe_open(os.path.join(ROOT, "output/moss/grams/flat.safetensors"), "pt")
|
| 25 |
+
gg = safe_open(os.path.join(ROOT, "output/moss/grams/gen.safetensors"), "pt")
|
| 26 |
+
kij = safe_open(os.path.join(ROOT, "models/loras/moss_image_sft_lora_blocks_21_to_49_rank_512_bf16.safetensors"), "pt")
|
| 27 |
+
kij_keys = set(kij.keys())
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def explicit(D, delta, Gf, Gg, DtD=None):
|
| 31 |
+
r = {"w2": float(D.pow(2).sum()), "dot": float((D * delta).sum())}
|
| 32 |
+
if Gf is not None:
|
| 33 |
+
DtD = D.T @ D if DtD is None else DtD # tr(D G D^T) = <G, D^T D>
|
| 34 |
+
r["ef"] = float((DtD * Gf).sum())
|
| 35 |
+
r["eg"] = float((DtD * Gg).sum())
|
| 36 |
+
return r
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
rows = []
|
| 40 |
+
for i in range(42):
|
| 41 |
+
for name in PROJ:
|
| 42 |
+
k = f"layers.{i}.proj.{name}"
|
| 43 |
+
delta = base.get_tensor(k + ".weight").float() - sft.get_tensor(k + ".weight").float()
|
| 44 |
+
row = {"layer": i, "proj": name}
|
| 45 |
+
Gf = Gg = None
|
| 46 |
+
if 21 <= i <= 41:
|
| 47 |
+
Gf, Gg = gf.get_tensor(k + ".G").float(), gg.get_tensor(k + ".G").float()
|
| 48 |
+
DtD = delta.T @ delta if Gf is not None else None
|
| 49 |
+
row["revert"] = explicit(delta, delta, Gf, Gg, DtD)
|
| 50 |
+
lk = f"diffusion_model.{k}.lora_up.weight"
|
| 51 |
+
if lk in kij_keys: # Kijai's LoRA at strength -1 (no alpha: scale 1)
|
| 52 |
+
up, down = kij.get_tensor(lk).float(), kij.get_tensor(f"diffusion_model.{k}.lora_down.weight").float()
|
| 53 |
+
D = -(up @ down)
|
| 54 |
+
row["kijai_m1"] = explicit(D, delta, Gf, Gg, None if Gf is None else down.T @ (up.T @ up) @ down)
|
| 55 |
+
if Gf is not None:
|
| 56 |
+
lam, U = torch.linalg.eigh(Gf)
|
| 57 |
+
lam = lam.clamp(min=0)
|
| 58 |
+
P = U.T @ DtD @ U
|
| 59 |
+
M = U.T @ Gg @ U
|
| 60 |
+
q = P.diagonal()
|
| 61 |
+
d = lam.numel()
|
| 62 |
+
for rho in RHOS:
|
| 63 |
+
f = lam / (lam + rho * lam.sum() / d)
|
| 64 |
+
row[f"solve_rho{rho:g}"] = {"w2": float((f * f * q).sum()), "dot": float((f * q).sum()), "ef": float((f * f * lam * q).sum()),
|
| 65 |
+
"eg": float(((f[:, None] * M * f[None, :]) * P.T).sum())}
|
| 66 |
+
rows.append(row)
|
| 67 |
+
print(k, {t: round(v["w2"] / row["revert"]["w2"], 3) for t, v in row.items() if isinstance(v, dict)}, flush=True)
|
| 68 |
+
json.dump(rows, open(OUT, "w"))
|
| 69 |
+
print("wrote", OUT)
|
research/edit_anatomy_c.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Second pass for edit_anatomy.json: output-change energy on horizontal detail only (row-centred image activations, the
|
| 3 |
+
flatc / genc Grams), which leaves out the per-token colour/brightness/modulation direction that dominates the plain Grams.
|
| 4 |
+
|
| 5 |
+
comfyui_venv/bin/python workflows/moss/edit_anatomy_c.py (CPU, after edit_anatomy.py)
|
| 6 |
+
Adds efc / egc to revert, kijai_m1 and the two saved Gram-solve checkpoints (ckpt_rho0.1, ckpt_rho1) for layers 21-41.
|
| 7 |
+
"""
|
| 8 |
+
import json, os
|
| 9 |
+
import torch
|
| 10 |
+
from safetensors import safe_open
|
| 11 |
+
|
| 12 |
+
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
|
| 13 |
+
PATH = os.path.join(ROOT, "output", "moss", "edit_anatomy.json")
|
| 14 |
+
torch.set_num_threads(16)
|
| 15 |
+
|
| 16 |
+
sft = safe_open(os.path.join(ROOT, "models/diffusion_models/moss_image_sft_bf16.safetensors"), "pt")
|
| 17 |
+
base = safe_open(os.path.join(ROOT, "models/diffusion_models/moss_image_base_bf16.safetensors"), "pt")
|
| 18 |
+
gfc = safe_open(os.path.join(ROOT, "output/moss/grams/flatc.safetensors"), "pt")
|
| 19 |
+
ggc = safe_open(os.path.join(ROOT, "output/moss/grams/genc.safetensors"), "pt")
|
| 20 |
+
kij = safe_open(os.path.join(ROOT, "models/loras/moss_image_sft_lora_blocks_21_to_49_rank_512_bf16.safetensors"), "pt")
|
| 21 |
+
ck = {t: safe_open(os.path.join(ROOT, "models/loras", f"moss_lab_gramsolve_mu0_{t[5:]}_L21-41.safetensors"), "pt") for t in ("ckpt_rho0.1", "ckpt_rho1")}
|
| 22 |
+
|
| 23 |
+
rows = json.load(open(PATH))
|
| 24 |
+
for row in rows:
|
| 25 |
+
if not 21 <= row["layer"] <= 41:
|
| 26 |
+
continue
|
| 27 |
+
k = f"layers.{row['layer']}.proj.{row['proj']}"
|
| 28 |
+
Gf, Gg = gfc.get_tensor(k + ".G").float(), ggc.get_tensor(k + ".G").float()
|
| 29 |
+
delta = base.get_tensor(k + ".weight").float() - sft.get_tensor(k + ".weight").float()
|
| 30 |
+
up, down = kij.get_tensor(f"diffusion_model.{k}.lora_up.weight").float(), kij.get_tensor(f"diffusion_model.{k}.lora_down.weight").float()
|
| 31 |
+
edits = {"revert": (delta, delta.T @ delta), "kijai_m1": (-(up @ down), down.T @ (up.T @ up) @ down)}
|
| 32 |
+
for t, h in ck.items():
|
| 33 |
+
D = h.get_tensor(f"diffusion_model.{k}.diff").float()
|
| 34 |
+
edits[t] = (D, D.T @ D)
|
| 35 |
+
for t, (D, DtD) in edits.items():
|
| 36 |
+
row.setdefault(t, {"w2": float(D.pow(2).sum()), "dot": float((D * delta).sum())})
|
| 37 |
+
row[t]["efc"] = float((DtD * Gf).sum())
|
| 38 |
+
row[t]["egc"] = float((DtD * Gg).sum())
|
| 39 |
+
print(k, {t: round((row[t]["efc"] / row["revert"]["efc"]) ** 0.5, 3) for t in edits}, flush=True)
|
| 40 |
+
json.dump(rows, open(PATH, "w"))
|
| 41 |
+
print("updated", PATH)
|
research/export_gramsolve.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Export a MOSSGramSolve edit (D = Delta G_f (G_f + mu G_g + eps I)^-1 per linear) as a ComfyUI full-difference LoRA (.diff keys).
|
| 3 |
+
Solved per linear on the GPU in float32 (the ridge keeps it well conditioned for rho >= ~0.01); only one linear's tensors are resident.
|
| 4 |
+
|
| 5 |
+
comfyui_venv/bin/python workflows/moss/export_gramsolve.py <out_name> --mu 0 --rho 1.0 --layers 21-41
|
| 6 |
+
Writes models/loras/<out_name>.safetensors (bf16) and prints per-linear |D|/|Delta|.
|
| 7 |
+
"""
|
| 8 |
+
import argparse, os, sys
|
| 9 |
+
import torch
|
| 10 |
+
from safetensors import safe_open
|
| 11 |
+
from safetensors.torch import save_file
|
| 12 |
+
|
| 13 |
+
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
|
| 14 |
+
sys.path.insert(0, os.path.join(ROOT, "custom_nodes", "moss_block_lab"))
|
| 15 |
+
ap = argparse.ArgumentParser()
|
| 16 |
+
ap.add_argument("out_name")
|
| 17 |
+
ap.add_argument("--mu", type=float, default=0.0)
|
| 18 |
+
ap.add_argument("--rho", type=float, default=1.0)
|
| 19 |
+
ap.add_argument("--layers", default="21-41")
|
| 20 |
+
ap.add_argument("--flat", default="flat")
|
| 21 |
+
ap.add_argument("--gen", default="gen")
|
| 22 |
+
ap.add_argument("--device", default="cuda")
|
| 23 |
+
a = ap.parse_args()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def parse_layers(spec):
|
| 27 |
+
out = set()
|
| 28 |
+
for part in spec.split(","):
|
| 29 |
+
lo, _, hi = part.partition("-")
|
| 30 |
+
out.update(range(int(lo), int(hi or lo) + 1))
|
| 31 |
+
return sorted(out)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
G = os.path.join(ROOT, "output", "moss", "grams")
|
| 35 |
+
gf = safe_open(os.path.join(G, a.flat + ".safetensors"), "pt")
|
| 36 |
+
gg = safe_open(os.path.join(G, a.gen + ".safetensors"), "pt") if a.mu > 0 else None
|
| 37 |
+
fs = safe_open(os.path.join(ROOT, "models", "diffusion_models", "moss_image_sft_bf16.safetensors"), "pt")
|
| 38 |
+
fb = safe_open(os.path.join(ROOT, "models", "diffusion_models", "moss_image_base_bf16.safetensors"), "pt")
|
| 39 |
+
out = {}
|
| 40 |
+
for i in parse_layers(a.layers):
|
| 41 |
+
for name in ("qkv", "o", "ffn_up_gate", "ffn_down"):
|
| 42 |
+
k = f"layers.{i}.proj.{name}"
|
| 43 |
+
dev = a.device
|
| 44 |
+
delta = fb.get_tensor(k + ".weight").to(dev, torch.float32) - fs.get_tensor(k + ".weight").to(dev, torch.float32)
|
| 45 |
+
A = gf.get_tensor(k + ".G").to(dev)
|
| 46 |
+
rhs = A.clone()
|
| 47 |
+
if gg is not None:
|
| 48 |
+
A = A + a.mu * gg.get_tensor(k + ".G").to(dev)
|
| 49 |
+
A.diagonal().add_(a.rho * A.diagonal().sum() / A.shape[0])
|
| 50 |
+
D = torch.cholesky_solve(rhs @ delta.T, torch.linalg.cholesky(A)).T
|
| 51 |
+
out[f"diffusion_model.{k}.diff"] = D.to(torch.bfloat16).cpu().contiguous()
|
| 52 |
+
print(f"{k}: |D|/|Delta| = {float(D.norm() / delta.norm()):.3f}", flush=True)
|
| 53 |
+
path = os.path.join(ROOT, "models", "loras", a.out_name + ".safetensors")
|
| 54 |
+
save_file(out, path, metadata={"mu": str(a.mu), "rho": str(a.rho), "layers": a.layers, "grams": f"{a.flat},{a.gen}",
|
| 55 |
+
"recipe": "D = (W_base - W_sft) G_f (G_f + mu G_g + rho tr(.)/d I)^-1"})
|
| 56 |
+
print("wrote", path, round(os.path.getsize(path) / 1e9, 2), "GB")
|
research/gram_capture/flat_000_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_001_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_002_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_003_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_004_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_005_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_006_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_007_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_008_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_009_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_010_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_011_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_012_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_013_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_014_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_015_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_016_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_017_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_018_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_019_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_020_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_021_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_022_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_023_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_024_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_025_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_026_832x1216_00001_.png
ADDED
|
research/gram_capture/flat_027_1024x1024_00001_.png
ADDED
|
research/gram_capture/flat_028_1216x832_00001_.png
ADDED
|
research/gram_capture/flat_029_832x1216_00001_.png
ADDED
|