Spaces:
Running on Zero
Running on Zero
Update model_config.py
Browse files- model_config.py +1530 -271
model_config.py
CHANGED
|
@@ -1,299 +1,1558 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
},
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
}
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# ⚙️ Comfy & Model Config
|
| 54 |
-
# ==========================================
|
| 55 |
|
| 56 |
-
|
| 57 |
-
"
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
|
|
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
"
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
|
| 96 |
-
"
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
},
|
| 116 |
-
|
| 117 |
-
"krea2": {
|
| 118 |
-
"name": "Krea-2-Turbo",
|
| 119 |
-
"companion_downloads": [
|
| 120 |
-
("waveshaker/comfy_models", krea2_te_path),
|
| 121 |
-
("waveshaker/comfy_models", "kr2/vae/qwen_image_vae.safetensors"),
|
| 122 |
-
],
|
| 123 |
-
"local_te_filename": [krea2_te_file],
|
| 124 |
-
"local_vae_filename": "qwen_image_vae.safetensors",
|
| 125 |
-
"clip_type": "krea2",
|
| 126 |
-
"char_lora_prefix": "krea2/",
|
| 127 |
-
"defaults": { "steps": 8, "cfg": 1.0, "sampler": "euler_ancestral", "scheduler": "beta", "char_lora_strength": 1.0 },
|
| 128 |
-
|
| 129 |
-
"fallback_unets": {
|
| 130 |
-
"Krea 2 Turbo Int8 Convrot": {
|
| 131 |
-
"repo": "waveshaker/comfy_models",
|
| 132 |
-
"subfolder": "kr2/diffusion_models",
|
| 133 |
-
"filename": "krea2_turbo_int8_convrot.safetensors"
|
| 134 |
}
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
},
|
| 138 |
-
|
| 139 |
-
"dev_krea2": {
|
| 140 |
-
"name": "Krea2-Dev-Test",
|
| 141 |
-
"companion_downloads": [
|
| 142 |
-
("waveshaker/comfy_models", krea2_te_path),
|
| 143 |
-
("waveshaker/comfy_models", "kr2/vae/qwen_image_vae.safetensors"),
|
| 144 |
-
],
|
| 145 |
-
"local_te_filename": [krea2_te_file],
|
| 146 |
-
"local_vae_filename": "qwen_image_vae.safetensors",
|
| 147 |
-
"clip_type": "krea2",
|
| 148 |
-
"char_lora_prefix": "krea2/",
|
| 149 |
-
"defaults": { "steps": 8, "cfg": 1.0, "sampler": "euler_ancestral", "scheduler": "beta", "char_lora_strength": 1.0 },
|
| 150 |
-
|
| 151 |
-
"fallback_unets": {
|
| 152 |
-
"Krea 2 Turbo Int4 Convrot": {
|
| 153 |
-
"repo": "waveshaker/comfy_models",
|
| 154 |
-
"subfolder": "kr2/diffusion_models",
|
| 155 |
-
"filename": "Krea2-Turbo-int4-ConvRot-ComfyUI.safetensors"
|
| 156 |
}
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
"name": "Anima",
|
| 162 |
-
"companion_downloads": [
|
| 163 |
-
("waveshaker/comfy_models", "anima/text_encoders/Qwen3-0.6B_base-int8_convrot.safetensors"),
|
| 164 |
-
("waveshaker/comfy_models", "anima/vae/qwen_image_vae.safetensors"),
|
| 165 |
-
],
|
| 166 |
-
"local_te_filename": ["Qwen3-0.6B_base-int8_convrot.safetensors"],
|
| 167 |
-
"local_vae_filename": "qwen_image_vae.safetensors",
|
| 168 |
-
"clip_type": "stable_diffusion",
|
| 169 |
-
"char_lora_prefix": "anima/",
|
| 170 |
-
"defaults": { "steps": 8, "cfg": 1.0, "sampler": "euler", "scheduler": "simple", "char_lora_strength": 1.0 },
|
| 171 |
-
|
| 172 |
-
"fallback_unets": {
|
| 173 |
-
"Diving Anima V4 Int8 Convrot (Anima Turbo LoRA v0.2 merged)": {
|
| 174 |
-
"repo": "waveshaker/comfy_models",
|
| 175 |
-
"subfolder": "anima/diffusion_models",
|
| 176 |
-
"filename": "divingAnima_v40_turbo_int8_convrot.safetensors"
|
| 177 |
}
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
"Anima Int4 Convrot MSE": {
|
| 202 |
-
"repo": "waveshaker/comfy_models",
|
| 203 |
-
"subfolder": "anima/diffusion_models",
|
| 204 |
-
"filename": "animaINT4Quants_int4ConvrotMSE.safetensors"
|
| 205 |
}
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
}
|
|
|
|
|
|
|
| 229 |
},
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
(
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
},
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
"filename": "LCMV2-PONYplus-PAseer.safetensors"
|
| 260 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
}
|
| 262 |
-
}
|
| 263 |
-
}
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
"root_mapping": {
|
| 269 |
-
"moss": "moss",
|
| 270 |
-
"krea2": "krea2",
|
| 271 |
-
"anima": "anima",
|
| 272 |
-
"flux1_dev": "flux1_dev",
|
| 273 |
-
"pony": "pony",
|
| 274 |
-
"dev_moss": "dev_moss",
|
| 275 |
-
"dev_krea2": "dev_krea2",
|
| 276 |
-
"dev_anima": "dev_anima"
|
| 277 |
-
}
|
| 278 |
}
|
| 279 |
-
}
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
print(f"🛠️ [DEV_MODE = True] 开发环境:仅加载 {DEV_TARGET_MODEL}。")
|
| 286 |
-
TARGET_MODELS = DEV_TARGET_MODEL
|
| 287 |
-
else:
|
| 288 |
-
print(f"🚀 [DEV_MODE = False] 生产环境:加载 {PROD_TARGET_MODEL}。")
|
| 289 |
-
TARGET_MODELS = PROD_TARGET_MODEL
|
| 290 |
-
|
| 291 |
-
MODEL_CONFIG["models"] = {
|
| 292 |
-
k: v for k, v in MODEL_CONFIG["models"].items()
|
| 293 |
-
if k in TARGET_MODELS
|
| 294 |
-
}
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ==========================================================================
|
| 2 |
+
// 📝 TODO (Architecture / Enhancements)
|
| 3 |
+
// ==========================================================================
|
| 4 |
+
// 1. [Prompt Presets] 实现基于 ID 的 Prompt 远程动态加载
|
| 5 |
+
// - 方案:将超长 Prompt 抽离为集中的 JSON 文件(如: prompt-assets/presets.json)。
|
| 6 |
+
// - 逻辑:图库 (Gallery) 跳转时仅传递 ?id=xxx,前端 (Studio) 接收到 ID 后,跨域 fetch 该 JSON 并自动填充 Prompt 输入框。
|
| 7 |
+
// - 优点:彻底解决 URL 长度限制,实现前后端数据彻底解耦,方便统一维护。
|
| 8 |
+
// commit@70f742a386b9cbbfd6df774b19d727485ef5446f
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
// ==========================================================================
|
| 12 |
+
// 🛡️ Dynamic API Base URL
|
| 13 |
+
// ==========================================================================
|
| 14 |
+
const hostname = window.location.hostname;
|
| 15 |
+
let API_BASE_URL = "";
|
| 16 |
+
|
| 17 |
+
// --------------------------------------------------------------------------
|
| 18 |
+
// 🚨 GUI Space 专属:匿名池额度警告 (右上角悬浮 + 自动消失 + 手动关闭)
|
| 19 |
+
// --------------------------------------------------------------------------
|
| 20 |
+
if (hostname.includes("universal-lora-studio-gui")) {
|
| 21 |
+
setTimeout(() => {
|
| 22 |
+
if (document.getElementById('gui-anon-warning')) return;
|
| 23 |
+
|
| 24 |
+
const toast = document.createElement('div');
|
| 25 |
+
toast.id = 'gui-anon-warning';
|
| 26 |
+
toast.style.cssText = `
|
| 27 |
+
position: fixed;
|
| 28 |
+
top: 24px;
|
| 29 |
+
right: 24px;
|
| 30 |
+
background-color: rgba(42, 42, 42, 0.95);
|
| 31 |
+
color: #FF7F50;
|
| 32 |
+
border: 1px solid #444;
|
| 33 |
+
padding: 12px 16px;
|
| 34 |
+
border-radius: 8px;
|
| 35 |
+
font-size: 13px;
|
| 36 |
+
font-weight: 500;
|
| 37 |
+
z-index: 99999;
|
| 38 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 39 |
+
display: flex;
|
| 40 |
+
align-items: flex-start;
|
| 41 |
+
gap: 12px;
|
| 42 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
|
| 43 |
+
backdrop-filter: blur(8px);
|
| 44 |
+
animation: slideInRight 0.4s ease-out;
|
| 45 |
+
max-width: 320px;
|
| 46 |
+
`;
|
| 47 |
+
|
| 48 |
+
toast.innerHTML = `
|
| 49 |
+
<div style="flex: 1; line-height: 1.5; color: #ccc;">
|
| 50 |
+
⚠️ [For Testing Only] You are using the <b style="color: #FF7F50;">Anonymous Quota</b>. It depletes very quickly.
|
| 51 |
+
<div style="margin-top: 10px;">
|
| 52 |
+
<a href="https://huggingface.co/spaces/waveshaker/universal-lora-studio" target="_blank" style="display: inline-block; color: #fff; background: #FF7F50; padding: 6px 12px; border-radius: 4px; text-decoration: none; font-weight: bold; font-size: 12px;">
|
| 53 |
+
Switch to Studio ↗
|
| 54 |
+
</a>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
<button id="close-anon-warning" style="background: transparent; border: none; color: #888; font-size: 18px; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#888'">
|
| 58 |
+
×
|
| 59 |
+
</button>
|
| 60 |
+
`;
|
| 61 |
+
|
| 62 |
+
const style = document.createElement('style');
|
| 63 |
+
style.innerHTML = `
|
| 64 |
+
@keyframes slideInRight {
|
| 65 |
+
from { opacity: 0; transform: translateX(40px); }
|
| 66 |
+
to { opacity: 1; transform: translateX(0); }
|
| 67 |
+
}
|
| 68 |
+
`;
|
| 69 |
+
document.head.appendChild(style);
|
| 70 |
+
document.body.appendChild(toast);
|
| 71 |
+
|
| 72 |
+
// 封装关闭逻辑
|
| 73 |
+
const closeToast = () => {
|
| 74 |
+
if (!document.getElementById('gui-anon-warning')) return;
|
| 75 |
+
toast.style.opacity = '0';
|
| 76 |
+
toast.style.transform = 'translateX(40px)';
|
| 77 |
+
toast.style.transition = 'all 0.3s';
|
| 78 |
+
setTimeout(() => toast.remove(), 300);
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
// 1. 绑定手动关闭
|
| 82 |
+
document.getElementById('close-anon-warning').onclick = closeToast;
|
| 83 |
+
|
| 84 |
+
// 2. 绑定 10 秒自动关闭
|
| 85 |
+
setTimeout(closeToast, 10000);
|
| 86 |
+
|
| 87 |
+
}, 100);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// 1. 如果在 GUI 预览环境(哪怕带 .static 后缀)
|
| 91 |
+
if (hostname.includes("universal-lora-studio-gui")) {
|
| 92 |
+
API_BASE_URL = "https://waveshaker-universal-lora-studio-test.hf.space";
|
| 93 |
+
}
|
| 94 |
+
// 2. 如果在本地开发环境
|
| 95 |
+
else if (hostname === "localhost" || hostname === "127.0.0.1") {
|
| 96 |
+
API_BASE_URL = "https://waveshaker-universal-lora-studio-test.hf.space";
|
| 97 |
+
}
|
| 98 |
+
// 3. 如果在测试环境(test Space)
|
| 99 |
+
else if (hostname.includes("universal-lora-studio-test")) {
|
| 100 |
+
API_BASE_URL = ""; // 同源,走相对路径
|
| 101 |
+
}
|
| 102 |
+
// 4. 如果在线上环境(prod Space)
|
| 103 |
+
else if (hostname.includes("universal-lora-studio.hf.space") || hostname.includes("universal-lora-studio.static")) {
|
| 104 |
+
API_BASE_URL = ""; // 同源,走相对路径
|
| 105 |
+
}
|
| 106 |
+
// ==========================================================================
|
| 107 |
+
// 🔐 ZeroGPU Token Handshake
|
| 108 |
+
// ==========================================================================
|
| 109 |
+
const ZEROGPU_HEADERS_MSG = "supports-zerogpu-headers";
|
| 110 |
+
|
| 111 |
+
// 监听 HF 父窗口发来的握手信号
|
| 112 |
+
window.addEventListener("message", (event) => {
|
| 113 |
+
if (event.data === ZEROGPU_HEADERS_MSG) {
|
| 114 |
+
window.supports_zerogpu_headers = true;
|
| 115 |
+
}
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
// 向父窗口声明支持,触发后续的 token 索要流程
|
| 119 |
+
(function announceZeroGpuSupport() {
|
| 120 |
+
const hostname = window.location.hostname;
|
| 121 |
+
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) {
|
| 122 |
+
return; // 非 HF 环境,不执行
|
| 123 |
+
}
|
| 124 |
+
const origin = hostname.includes(".dev.")
|
| 125 |
+
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
|
| 126 |
+
: "https://huggingface.co";
|
| 127 |
+
try {
|
| 128 |
+
window.parent.postMessage(ZEROGPU_HEADERS_MSG, origin);
|
| 129 |
+
} catch (e) {
|
| 130 |
+
console.warn("ZeroGPU handshake failed:", e);
|
| 131 |
+
}
|
| 132 |
+
})();
|
| 133 |
+
|
| 134 |
+
// 向父窗口索要 x-ip-token(带 2 秒超时兜底)
|
| 135 |
+
async function requestZeroGpuToken() {
|
| 136 |
+
if (!window.supports_zerogpu_headers) return null;
|
| 137 |
+
|
| 138 |
+
const hostname = window.location.hostname;
|
| 139 |
+
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) return null;
|
| 140 |
+
|
| 141 |
+
const origin = hostname.includes(".dev.")
|
| 142 |
+
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
|
| 143 |
+
: "https://huggingface.co";
|
| 144 |
+
|
| 145 |
+
try {
|
| 146 |
+
return await new Promise((resolve) => {
|
| 147 |
+
const channel = new MessageChannel();
|
| 148 |
+
const timeout = setTimeout(() => {
|
| 149 |
+
channel.port1.close();
|
| 150 |
+
resolve(null);
|
| 151 |
+
}, 2000);
|
| 152 |
+
channel.port1.onmessage = ({ data }) => {
|
| 153 |
+
clearTimeout(timeout);
|
| 154 |
+
resolve(data || null);
|
| 155 |
+
};
|
| 156 |
+
window.parent.postMessage("zerogpu-headers", origin, [channel.port2]);
|
| 157 |
+
});
|
| 158 |
+
} catch {
|
| 159 |
+
return null;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// ==========================================================================
|
| 164 |
+
// 🛡️ API Layer
|
| 165 |
+
// ==========================================================================
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
const BackendAPI = {
|
| 169 |
+
// 1. 心跳检测
|
| 170 |
+
checkHealth: async function() {
|
| 171 |
+
try {
|
| 172 |
+
const controller = new AbortController();
|
| 173 |
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
| 174 |
+
|
| 175 |
+
const res = await fetch(`${API_BASE_URL}/api/health`, {
|
| 176 |
+
signal: controller.signal,
|
| 177 |
+
cache: 'no-store'
|
| 178 |
+
});
|
| 179 |
+
clearTimeout(timeoutId);
|
| 180 |
+
|
| 181 |
+
if (res.ok) {
|
| 182 |
+
const data = await res.json();
|
| 183 |
+
return data.status === "ok";
|
| 184 |
+
}
|
| 185 |
+
return false;
|
| 186 |
+
} catch (error) {
|
| 187 |
+
return false;
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
|
| 191 |
+
// 2. 获取全局配置 (Sampler/Scheduler & Root Mapping)
|
| 192 |
+
getConfig: async function() {
|
| 193 |
+
try {
|
| 194 |
+
const res = await fetch(`${API_BASE_URL}/api/config`, {
|
| 195 |
+
method: 'GET',
|
| 196 |
+
cache: 'no-store'
|
| 197 |
+
});
|
| 198 |
+
if (!res.ok) throw new Error("HTTP error " + res.status);
|
| 199 |
+
return await res.json();
|
| 200 |
+
} catch (error) {
|
| 201 |
+
console.warn("⚠️ Failed to fetch config", error);
|
| 202 |
+
return null;
|
| 203 |
+
}
|
| 204 |
},
|
| 205 |
+
|
| 206 |
+
// 3. 获取 Base Model 列表
|
| 207 |
+
getModels: async function() {
|
| 208 |
+
const res = await fetch(`${API_BASE_URL}/api/models`, {
|
| 209 |
+
method: 'GET',
|
| 210 |
+
cache: 'no-store'
|
| 211 |
+
});
|
| 212 |
+
if (!res.ok) throw new Error("HTTP error " + res.status);
|
| 213 |
+
return await res.json();
|
| 214 |
},
|
| 215 |
+
|
| 216 |
+
// 4. 获取角色 LoRA 图库数据
|
| 217 |
+
getLoras: async function() {
|
| 218 |
+
const controller = new AbortController();
|
| 219 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
| 220 |
+
|
| 221 |
+
try {
|
| 222 |
+
const res = await fetch(`${API_BASE_URL}/api/loras`, {
|
| 223 |
+
method: 'GET',
|
| 224 |
+
cache: 'no-store',
|
| 225 |
+
signal: controller.signal
|
| 226 |
+
});
|
| 227 |
+
clearTimeout(timeoutId);
|
| 228 |
+
if (!res.ok) throw new Error("HTTP error " + res.status);
|
| 229 |
+
return await res.json();
|
| 230 |
+
} catch (error) {
|
| 231 |
+
clearTimeout(timeoutId);
|
| 232 |
+
throw error;
|
| 233 |
+
}
|
| 234 |
+
},
|
| 235 |
+
|
| 236 |
+
// 5. 提交生成任务
|
| 237 |
+
submitGenerate: async function(payload) {
|
| 238 |
+
const headers = { 'Content-Type': 'application/json' };
|
| 239 |
+
|
| 240 |
+
const tokenInfo = await requestZeroGpuToken();
|
| 241 |
+
if (tokenInfo) {
|
| 242 |
+
if (typeof tokenInfo === 'object') {
|
| 243 |
+
Object.assign(headers, tokenInfo);
|
| 244 |
+
} else if (typeof tokenInfo === 'string') {
|
| 245 |
+
headers['x-ip-token'] = tokenInfo;
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
const res = await fetch(`${API_BASE_URL}/api/generate`, {
|
| 250 |
+
method: 'POST',
|
| 251 |
+
cache: 'no-store',
|
| 252 |
+
headers: headers,
|
| 253 |
+
body: JSON.stringify(payload)
|
| 254 |
+
});
|
| 255 |
+
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
| 256 |
+
const data = await res.json();
|
| 257 |
+
if (data.status === "error") throw new Error(data.message);
|
| 258 |
+
return data.task_id;
|
| 259 |
+
},
|
| 260 |
+
|
| 261 |
+
// 6. 轮询任务状态
|
| 262 |
+
pollStatus: async function(taskId, callbacks, retries = 0) {
|
| 263 |
+
const MAX_RETRIES = 150;
|
| 264 |
+
|
| 265 |
+
try {
|
| 266 |
+
const res = await fetch(`${API_BASE_URL}/api/status/${taskId}`, {
|
| 267 |
+
cache: 'no-store'
|
| 268 |
+
});
|
| 269 |
+
const data = await res.json();
|
| 270 |
+
|
| 271 |
+
if (data.status === "processing" || data.status === "queued") {
|
| 272 |
+
if (callbacks.onProcessing) {
|
| 273 |
+
callbacks.onProcessing(data.message || "Processing...");
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
if (retries >= MAX_RETRIES) {
|
| 277 |
+
const timeoutMsg = `Timeout: Backend exceeded ${MAX_RETRIES * 500 / 1000}s. Please try again.`;
|
| 278 |
+
if (callbacks.onError) callbacks.onError(timeoutMsg);
|
| 279 |
+
return;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
setTimeout(() => this.pollStatus(taskId, callbacks, retries + 1), 100);
|
| 283 |
+
}
|
| 284 |
+
else if (data.status === "success") {
|
| 285 |
+
// 如果后端返回的是绝对路径或 data: 开头,直接使用;否则拼接 API_BASE_URL
|
| 286 |
+
let finalImageUrl = data.image;
|
| 287 |
+
if (finalImageUrl && !finalImageUrl.startsWith('data:') && !finalImageUrl.startsWith('http')) {
|
| 288 |
+
finalImageUrl = `${API_BASE_URL}${finalImageUrl}`;
|
| 289 |
+
}
|
| 290 |
+
if (callbacks.onSuccess) callbacks.onSuccess(finalImageUrl, data.seed, data.metrics, data.filename);
|
| 291 |
+
}
|
| 292 |
+
else if (data.status === "error") {
|
| 293 |
+
if (callbacks.onError) callbacks.onError(data.message);
|
| 294 |
+
}
|
| 295 |
+
else if (data.status === "not_found") {
|
| 296 |
+
if (callbacks.onError) callbacks.onError("Task not found (backend may have restarted).");
|
| 297 |
+
}
|
| 298 |
+
} catch (error) {
|
| 299 |
+
if (callbacks.onError) callbacks.onError(error.message);
|
| 300 |
+
}
|
| 301 |
+
},
|
| 302 |
+
};
|
| 303 |
+
|
| 304 |
+
// ==========================================================================
|
| 305 |
+
// 🧠 Global State & Utilities
|
| 306 |
+
// ==========================================================================
|
| 307 |
+
let generateStartTime = 0;
|
| 308 |
+
let currentStageStartTime = 0;
|
| 309 |
+
let currentStageName = "";
|
| 310 |
+
let isInitializing = false;
|
| 311 |
+
let shouldAnimateNextRender = false;
|
| 312 |
+
|
| 313 |
+
let globalModelData = {};
|
| 314 |
+
let currentGalleryData = [];
|
| 315 |
+
let renderedCount = 0;
|
| 316 |
+
const BATCH_SIZE = 20;
|
| 317 |
+
let selectedLora = "";
|
| 318 |
+
let backendState = { isReady: false, isChecking: false, healthInterval: null };
|
| 319 |
+
|
| 320 |
+
const ANIM_CONFIG = {
|
| 321 |
+
startY: 24,
|
| 322 |
+
stiffness: 280,
|
| 323 |
+
damping: 0.7,
|
| 324 |
+
staggerDelay: 30,
|
| 325 |
+
|
| 326 |
+
};
|
| 327 |
+
|
| 328 |
+
function toUnicodeBold(str) {
|
| 329 |
+
return str.split('').map(char => {
|
| 330 |
+
const code = char.charCodeAt(0);
|
| 331 |
+
if (code >= 65 && code <= 90) return String.fromCodePoint(0x1D5D4 + (code - 65));
|
| 332 |
+
if (code >= 97 && code <= 122) return String.fromCodePoint(0x1D5EE + (code - 97));
|
| 333 |
+
if (code >= 48 && code <= 57) return String.fromCodePoint(0x1D7EC + (code - 48));
|
| 334 |
+
return char;
|
| 335 |
+
}).join('');
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function bindSlider(sliderId, inputId) {
|
| 339 |
+
const slider = document.getElementById(sliderId);
|
| 340 |
+
const input = document.getElementById(inputId);
|
| 341 |
+
|
| 342 |
+
// 1. 滑动滑块 -> 数字框实时跟随(这步肯定没问题)
|
| 343 |
+
slider.addEventListener('input', (e) => {
|
| 344 |
+
input.value = e.target.value;
|
| 345 |
+
});
|
| 346 |
+
|
| 347 |
+
// 2. 数���框输入中 -> 实时同步给滑块,但不强制打断用户输入
|
| 348 |
+
input.addEventListener('input', (e) => {
|
| 349 |
+
let val = parseFloat(e.target.value);
|
| 350 |
+
// 只有合法且没超范围,才同步给滑块
|
| 351 |
+
if (!isNaN(val) && val >= parseFloat(slider.min) && val <= parseFloat(slider.max)) {
|
| 352 |
+
slider.value = val;
|
| 353 |
+
}
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
// 3. 输入结束(失焦或回车)-> 执行严格的边界校验和回滚
|
| 357 |
+
const enforceLimits = () => {
|
| 358 |
+
let val = parseFloat(input.value);
|
| 359 |
+
const min = parseFloat(slider.min);
|
| 360 |
+
const max = parseFloat(slider.max);
|
| 361 |
+
|
| 362 |
+
if (isNaN(val)) {
|
| 363 |
+
input.value = min; // 如果清空了,默认回到最小值
|
| 364 |
+
slider.value = min;
|
| 365 |
+
return;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
if (val < min) {
|
| 369 |
+
input.value = min;
|
| 370 |
+
slider.value = min;
|
| 371 |
+
} else if (val > max) {
|
| 372 |
+
input.value = max;
|
| 373 |
+
slider.value = max;
|
| 374 |
+
} else {
|
| 375 |
+
slider.value = val; // 防一手用户输入了 1024,滑块没变
|
| 376 |
+
}
|
| 377 |
+
};
|
| 378 |
+
|
| 379 |
+
input.addEventListener('change', enforceLimits);
|
| 380 |
+
input.addEventListener('blur', enforceLimits);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
bindSlider('width-slider', 'width-input');
|
| 384 |
+
bindSlider('height-slider', 'height-input');
|
| 385 |
+
bindSlider('steps-slider', 'steps-input');
|
| 386 |
+
bindSlider('cfg-slider', 'cfg-input');
|
| 387 |
+
|
| 388 |
+
const RATIO_PRESETS = {
|
| 389 |
+
"1:1": { w: 1024, h: 1024 },
|
| 390 |
+
"3:4": { w: 888, h: 1184 },
|
| 391 |
+
"9:16": { w: 768, h: 1368 },
|
| 392 |
+
"4:3": { w: 1184, h: 888 },
|
| 393 |
+
"16:9": { w: 1368, h: 768 }
|
| 394 |
+
};
|
| 395 |
+
|
| 396 |
+
document.querySelectorAll('.ratio-btn').forEach(btn => {
|
| 397 |
+
btn.addEventListener('click', () => {
|
| 398 |
+
document.querySelectorAll('.ratio-btn').forEach(b => b.classList.remove('active'));
|
| 399 |
+
btn.classList.add('active');
|
| 400 |
+
|
| 401 |
+
const ratio = btn.dataset.ratio;
|
| 402 |
+
const preset = RATIO_PRESETS[ratio];
|
| 403 |
+
if (preset) {
|
| 404 |
+
document.getElementById('width-slider').value = preset.w;
|
| 405 |
+
document.getElementById('width-input').value = preset.w;
|
| 406 |
+
document.getElementById('height-slider').value = preset.h;
|
| 407 |
+
document.getElementById('height-input').value = preset.h;
|
| 408 |
+
}
|
| 409 |
+
});
|
| 410 |
+
});
|
| 411 |
+
|
| 412 |
+
function clearActiveRatio() {
|
| 413 |
+
document.querySelectorAll('.ratio-btn').forEach(b => b.classList.remove('active'));
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
document.getElementById('width-slider').addEventListener('input', clearActiveRatio);
|
| 417 |
+
document.getElementById('width-input').addEventListener('input', clearActiveRatio);
|
| 418 |
+
document.getElementById('height-slider').addEventListener('input', clearActiveRatio);
|
| 419 |
+
document.getElementById('height-input').addEventListener('input', clearActiveRatio);
|
| 420 |
+
|
| 421 |
+
function setUIState(isGenerating) {
|
| 422 |
+
const btn = document.getElementById('generate-btn');
|
| 423 |
+
if (!backendState.isReady) {
|
| 424 |
+
btn.disabled = true;
|
| 425 |
+
btn.innerText = "⏳ Waiting for Backend...";
|
| 426 |
+
} else if (isGenerating) {
|
| 427 |
+
btn.disabled = true;
|
| 428 |
+
btn.innerText = "⏳ Generating...";
|
| 429 |
+
} else {
|
| 430 |
+
btn.disabled = false;
|
| 431 |
+
btn.innerText = "Generate";
|
| 432 |
}
|
| 433 |
}
|
| 434 |
|
| 435 |
+
// 获取 Checkbox 元素
|
| 436 |
+
const autoScrollToggle = document.getElementById('auto-scroll-toggle');
|
| 437 |
+
|
| 438 |
+
function smartScroll() {
|
| 439 |
+
// 只有用户勾选了“自动滚动”,才强制贴底
|
| 440 |
+
if (autoScrollToggle && autoScrollToggle.checked) {
|
| 441 |
+
const logContent = document.getElementById('log-content');
|
| 442 |
+
// 用 requestAnimationFrame 确保在 DOM 高度更新后再执行滚动
|
| 443 |
+
requestAnimationFrame(() => {
|
| 444 |
+
logContent.scrollTop = logContent.scrollHeight;
|
| 445 |
+
});
|
| 446 |
+
}
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function addLog(message, type = "normal") {
|
| 450 |
+
const logContent = document.getElementById('log-content');
|
|
|
|
|
|
|
| 451 |
|
| 452 |
+
const newLine = document.createElement('div');
|
| 453 |
+
newLine.className = `log-line ${type === "normal" ? "" : type}`;
|
| 454 |
|
| 455 |
+
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
|
| 456 |
+
newLine.innerText = `[${time}] ${message}`;
|
| 457 |
+
logContent.appendChild(newLine);
|
| 458 |
|
| 459 |
+
if (logContent.childElementCount > 100) {
|
| 460 |
+
logContent.firstElementChild.remove();
|
| 461 |
+
}
|
| 462 |
|
| 463 |
+
// 插入后调用智能滚动
|
| 464 |
+
smartScroll();
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
// ==========================================================================
|
| 468 |
+
// 💓 Health Check & Initialization
|
| 469 |
+
// ==========================================================================
|
| 470 |
+
async function performHealthCheck() {
|
| 471 |
+
if (backendState.isChecking) return;
|
| 472 |
+
backendState.isChecking = true;
|
| 473 |
+
|
| 474 |
+
const dot = document.getElementById('backend-status-dot');
|
| 475 |
+
const wrapper = document.getElementById('backend-status-wrapper');
|
| 476 |
|
| 477 |
+
if (!backendState.isReady) {
|
| 478 |
+
setUIState(false);
|
| 479 |
+
dot.className = "status-dot loading";
|
| 480 |
+
wrapper.title = "Connecting to backend...";
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
const isHealthy = await BackendAPI.checkHealth();
|
| 484 |
+
|
| 485 |
+
if (isHealthy) {
|
| 486 |
+
if (!backendState.isReady) {
|
| 487 |
+
backendState.isReady = true;
|
| 488 |
+
setUIState(false);
|
| 489 |
+
dot.className = "status-dot online";
|
| 490 |
+
wrapper.title = "Backend online, ready to generate!";
|
| 491 |
+
await loadInitialData();
|
| 492 |
+
}
|
| 493 |
+
} else {
|
| 494 |
+
backendState.isReady = false;
|
| 495 |
+
setUIState(false);
|
| 496 |
+
dot.className = "status-dot offline";
|
| 497 |
+
wrapper.title = "Backend offline, trying to reconnect...";
|
| 498 |
+
}
|
| 499 |
+
backendState.isChecking = false;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
function startHealthPolling() {
|
| 503 |
+
if (backendState.healthInterval) return;
|
| 504 |
+
backendState.healthInterval = setInterval(async () => {
|
| 505 |
+
if (backendState.isReady) {
|
| 506 |
+
clearInterval(backendState.healthInterval);
|
| 507 |
+
backendState.healthInterval = null;
|
| 508 |
+
return;
|
| 509 |
+
}
|
| 510 |
+
await performHealthCheck();
|
| 511 |
+
}, 3000);
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
async function loadInitialData() {
|
| 515 |
+
if (isInitializing) return;
|
| 516 |
+
isInitializing = true;
|
| 517 |
+
|
| 518 |
+
console.log("🟢 [Init] 开始初始化...");
|
| 519 |
+
document.getElementById('unet-select').disabled = true;
|
| 520 |
+
|
| 521 |
+
const config = await BackendAPI.getConfig();
|
| 522 |
+
if (!config) console.error("❌ 获取配置失败,请检查后端 API 是否正常");
|
| 523 |
+
|
| 524 |
+
const finalSamplers = config?.samplers || ["euler", "euler_ancestral", "euler_a", "dpmpp_2m", "dpmpp_2m_sde", "dpmpp_sde", "heun", "lms", "lcm"];
|
| 525 |
+
const finalSchedulers = config?.schedulers || ["simple", "beta", "normal", "karras", "exponential", "sgm_uniform"];
|
| 526 |
+
const rootMapping = config?.root_mapping || {};
|
| 527 |
+
|
| 528 |
+
if (config && config.default_prompt) {
|
| 529 |
+
document.getElementById('prompt').value = config.default_prompt;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
document.getElementById('sampler').innerHTML = finalSamplers.map(s => `<option value="${s}">${s}</option>`).join('');
|
| 533 |
+
document.getElementById('scheduler').innerHTML = finalSchedulers.map(s => `<option value="${s}">${s}</option>`).join('');
|
| 534 |
+
|
| 535 |
+
const selectEl = document.getElementById('unet-select');
|
| 536 |
+
try {
|
| 537 |
+
const data = await BackendAPI.getModels();
|
| 538 |
+
console.log("🟢 [Init] Models 数据已拉取");
|
| 539 |
+
|
| 540 |
+
if (data.models) {
|
| 541 |
+
globalModelData = data.models;
|
| 542 |
+
selectEl.innerHTML = "";
|
| 543 |
+
Object.keys(data.models).forEach(modelName => {
|
| 544 |
+
const opt = document.createElement("option");
|
| 545 |
+
opt.value = modelName;
|
| 546 |
+
|
| 547 |
+
if (modelName.includes(" | ")) {
|
| 548 |
+
const parts = modelName.split(" | ");
|
| 549 |
+
const enginePart = toUnicodeBold(parts[0]);
|
| 550 |
+
const modelPart = parts[1];
|
| 551 |
+
opt.textContent = `${enginePart} | ${modelPart}`;
|
| 552 |
+
} else {
|
| 553 |
+
opt.textContent = toUnicodeBold(modelName);
|
| 554 |
+
}
|
| 555 |
+
selectEl.appendChild(opt);
|
| 556 |
+
});
|
| 557 |
+
|
| 558 |
+
let modelParam = null;
|
| 559 |
+
let loraParam = null;
|
| 560 |
+
|
| 561 |
+
if (window.location.hash) {
|
| 562 |
+
const hashStr = window.location.hash.substring(1);
|
| 563 |
+
const hashParams = new URLSearchParams(hashStr);
|
| 564 |
+
modelParam = hashParams.get('model');
|
| 565 |
+
loraParam = hashParams.get('lora');
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
if (!modelParam) {
|
| 569 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 570 |
+
modelParam = urlParams.get('model');
|
| 571 |
+
loraParam = urlParams.get('lora');
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
let targetModel = Object.keys(data.models)[0];
|
| 575 |
+
|
| 576 |
+
if (modelParam) {
|
| 577 |
+
const targetEngine = rootMapping[modelParam] || modelParam;
|
| 578 |
+
console.log(`🔗 URL解析: ${modelParam} -> ${targetEngine}`);
|
| 579 |
+
for (const key of Object.keys(data.models)) {
|
| 580 |
+
const modelEngine = data.models[key].engine;
|
| 581 |
+
if (modelEngine === targetEngine) {
|
| 582 |
+
targetModel = key;
|
| 583 |
+
console.log(`✅ 成功匹配到模型: ${key} (Engine: ${modelEngine})`);
|
| 584 |
+
break;
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
console.log(`🟢 [Init] 准备选中模型: ${targetModel}`);
|
| 590 |
+
selectEl.value = targetModel;
|
| 591 |
+
window._currentSelectedModel = targetModel;
|
| 592 |
+
window._lastUsedArchitecture = globalModelData[targetModel]?.engine || "unknown";
|
| 593 |
+
|
| 594 |
+
const modelInfo = globalModelData[targetModel];
|
| 595 |
+
const infoEl = document.getElementById('model-assets-info');
|
| 596 |
+
if (modelInfo && infoEl) {
|
| 597 |
+
const te = modelInfo.te_name || "Unknown";
|
| 598 |
+
const vae = modelInfo.vae_name || "Unknown";
|
| 599 |
+
infoEl.innerHTML = `
|
| 600 |
+
<div class="te-vae-container">
|
| 601 |
+
<div class="te-wrapper">
|
| 602 |
+
<span class="te-label">TE: </span>
|
| 603 |
+
<span class="te-value">${te}</span>
|
| 604 |
+
</div>
|
| 605 |
+
<div class="vae-wrapper">
|
| 606 |
+
<span class="vae-label">VAE: </span>
|
| 607 |
+
<span class="vae-value">${vae}</span>
|
| 608 |
+
</div>
|
| 609 |
+
</div>
|
| 610 |
+
`;
|
| 611 |
+
}
|
| 612 |
|
| 613 |
+
const modelDefaults = globalModelData[targetModel]?.defaults || {};
|
| 614 |
+
|
| 615 |
+
if (globalModelData[targetModel]?.default_prompt !== undefined) {
|
| 616 |
+
document.getElementById('prompt').value = globalModelData[targetModel].default_prompt;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
if (modelDefaults.cfg !== undefined) {
|
| 620 |
+
document.getElementById('cfg-slider').value = modelDefaults.cfg;
|
| 621 |
+
document.getElementById('cfg-input').value = Number(modelDefaults.cfg).toFixed(1);
|
| 622 |
+
}
|
| 623 |
+
if (modelDefaults.steps !== undefined) {
|
| 624 |
+
document.getElementById('steps-slider').value = modelDefaults.steps;
|
| 625 |
+
document.getElementById('steps-input').value = modelDefaults.steps;
|
| 626 |
+
}
|
| 627 |
+
if (modelDefaults.sampler !== undefined) document.getElementById('sampler').value = modelDefaults.sampler;
|
| 628 |
+
if (modelDefaults.scheduler !== undefined) document.getElementById('scheduler').value = modelDefaults.scheduler;
|
| 629 |
+
|
| 630 |
+
if (modelDefaults.char_lora_strength !== undefined) {
|
| 631 |
+
document.getElementById('char-strength').value = Number(modelDefaults.char_lora_strength).toFixed(1);
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
selectedLora = "";
|
| 635 |
+
document.getElementById('selected-lora-name').innerText = "None";
|
| 636 |
+
document.getElementById('selected-lora-name').style.color = 'var(--text-muted)';
|
| 637 |
+
const bigImg = document.getElementById('selected-lora-big-img');
|
| 638 |
+
const placeholder = document.getElementById('preview-placeholder');
|
| 639 |
+
bigImg.src = "";
|
| 640 |
+
bigImg.style.display = 'none';
|
| 641 |
+
placeholder.style.display = 'flex';
|
| 642 |
+
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
|
| 643 |
+
|
| 644 |
+
renderBaseLoras(targetModel);
|
| 645 |
+
|
| 646 |
+
window._isDataReady = true;
|
| 647 |
+
// 🌟 核心修改:如果 URL 没有指定 lora 参数,说明是普通进入,开启动画
|
| 648 |
+
const isPlainEntry = !loraParam;
|
| 649 |
+
await fetchAndRenderGallery(targetModel, isPlainEntry);
|
| 650 |
+
|
| 651 |
+
if (loraParam) {
|
| 652 |
+
if (!loraParam.endsWith(".safetensors")) {
|
| 653 |
+
console.warn(`⚠️ [Init] 忽略无效的 LoRA 参数(缺少 .safetensors 后缀): ${loraParam}`);
|
| 654 |
+
} else {
|
| 655 |
+
const targetIndex = currentGalleryData.findIndex(item => item.filename === loraParam);
|
| 656 |
+
|
| 657 |
+
if (targetIndex !== -1) {
|
| 658 |
+
const targetItem = currentGalleryData[targetIndex];
|
| 659 |
+
console.log(`🟢 [Init] 数据层成功找到 LoRA: ${loraParam},位于第 ${targetIndex + 1} 个`);
|
| 660 |
+
|
| 661 |
+
selectedLora = targetItem.filename;
|
| 662 |
+
const nameEl = document.getElementById('selected-lora-name');
|
| 663 |
+
nameEl.innerText = targetItem.title;
|
| 664 |
+
nameEl.style.color = 'var(--accent)';
|
| 665 |
+
|
| 666 |
+
const bigImg = document.getElementById('selected-lora-big-img');
|
| 667 |
+
const placeholder = document.getElementById('preview-placeholder');
|
| 668 |
+
const previewWrapper = document.querySelector('.preview-wrapper');
|
| 669 |
+
|
| 670 |
+
if (previewWrapper) previewWrapper.classList.add('shimmer-container');
|
| 671 |
+
|
| 672 |
+
bigImg.src = targetItem.webp;
|
| 673 |
+
|
| 674 |
+
bigImg.onload = () => {
|
| 675 |
+
bigImg.style.display = 'block';
|
| 676 |
+
placeholder.style.display = 'none';
|
| 677 |
+
if (previewWrapper) previewWrapper.classList.remove('shimmer-container');
|
| 678 |
+
bigImg.onload = null;
|
| 679 |
+
};
|
| 680 |
+
|
| 681 |
+
bigImg.onerror = () => {
|
| 682 |
+
if (previewWrapper) previewWrapper.classList.remove('shimmer-container');
|
| 683 |
+
bigImg.onerror = null;
|
| 684 |
+
};
|
| 685 |
+
|
| 686 |
+
if (targetIndex >= renderedCount) {
|
| 687 |
+
const requiredRenderCount = Math.ceil((targetIndex + 1) / BATCH_SIZE) * BATCH_SIZE;
|
| 688 |
+
const tempData = currentGalleryData.slice(renderedCount, requiredRenderCount);
|
| 689 |
+
|
| 690 |
+
const grid = document.getElementById('gallery-grid');
|
| 691 |
+
tempData.forEach(item => {
|
| 692 |
+
const card = document.createElement('div');
|
| 693 |
+
card.className = 'lora-card';
|
| 694 |
+
card.dataset.filename = item.filename;
|
| 695 |
+
card.innerHTML = `
|
| 696 |
+
<img src="${item.webp}" loading="lazy" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x400?text=No+Image'">
|
| 697 |
+
<div class="card-info">
|
| 698 |
+
<div class="card-title">${item.title}</div>
|
| 699 |
+
</div>
|
| 700 |
+
`;
|
| 701 |
+
|
| 702 |
+
const cardImg = card.querySelector('img');
|
| 703 |
+
applyCardShimmer(card, cardImg);
|
| 704 |
+
bindCardClick(card, item);
|
| 705 |
+
grid.appendChild(card);
|
| 706 |
+
});
|
| 707 |
+
renderedCount = requiredRenderCount;
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
setTimeout(() => {
|
| 711 |
+
const targetCard = document.querySelector(`.lora-card[data-filename="${loraParam}"]`);
|
| 712 |
+
if (targetCard) {
|
| 713 |
+
console.log(`🟢 [Init] DOM 已渲染,执行滚动和高亮`);
|
| 714 |
+
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
|
| 715 |
+
targetCard.classList.add('selected');
|
| 716 |
+
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 717 |
+
} else {
|
| 718 |
+
console.warn(`⚠️ 极度异常:数据存在,但强制渲染后依然找不到 DOM`);
|
| 719 |
+
}
|
| 720 |
+
}, 150);
|
| 721 |
+
|
| 722 |
+
} else {
|
| 723 |
+
console.warn(`⚠️ [Init] 在返回的数据中不存在该 LoRA: ${loraParam}`);
|
| 724 |
+
}
|
| 725 |
}
|
| 726 |
+
}
|
| 727 |
+
}
|
| 728 |
+
} catch (e) {
|
| 729 |
+
console.error("❌ 初始化加载失败:", e);
|
| 730 |
+
selectEl.innerHTML = `<option value="">🔴 Backend Offline</option>`;
|
| 731 |
+
} finally {
|
| 732 |
+
document.getElementById('unet-select').disabled = false;
|
| 733 |
+
|
| 734 |
+
// 🌟 智能移除遮罩:如果数据没就绪,就等它就绪
|
| 735 |
+
const waitForDataAndHideMask = setInterval(() => {
|
| 736 |
+
if (window._isDataReady) {
|
| 737 |
+
clearInterval(waitForDataAndHideMask);
|
| 738 |
+
|
| 739 |
+
const mask = document.getElementById('global-loading-mask');
|
| 740 |
+
if (mask) {
|
| 741 |
+
setTimeout(() => {
|
| 742 |
+
mask.style.opacity = '0';
|
| 743 |
+
setTimeout(() => mask.remove(), 300);
|
| 744 |
+
}, 100); // 给 DOM 留 100ms 的渲染时间
|
| 745 |
+
}
|
| 746 |
+
}
|
| 747 |
+
}, 100);
|
| 748 |
+
}
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
// ==========================================================================
|
| 752 |
+
// 🖼️ Gallery & Card Rendering
|
| 753 |
+
// ==========================================================================
|
| 754 |
+
function renderBaseLoras(selectedModel) {
|
| 755 |
+
const container = document.getElementById('base-lora-container');
|
| 756 |
+
container.innerHTML = "";
|
| 757 |
+
const loras = globalModelData[selectedModel]?.base_loras || [];
|
| 758 |
+
if (loras.length === 0) {
|
| 759 |
+
container.innerHTML = `<div style="font-size: 12px; color: var(--text-muted);">No Base LoRA for current model</div>`;
|
| 760 |
+
return;
|
| 761 |
+
}
|
| 762 |
+
loras.forEach((lora) => {
|
| 763 |
+
const div = document.createElement('div');
|
| 764 |
+
div.className = 'lora-row';
|
| 765 |
+
div.innerHTML = `
|
| 766 |
+
<label>
|
| 767 |
+
<input type="checkbox" class="base-lora-cb" ${lora.defaultEnable ? 'checked' : ''}>
|
| 768 |
+
${lora.name}
|
| 769 |
+
</label>
|
| 770 |
+
<input type="number" class="strength-box base-lora-strength" value="1.0" step="0.05" title="Strength">
|
| 771 |
+
`;
|
| 772 |
+
container.appendChild(div);
|
| 773 |
+
});
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
// 在全局变量区加一个请求锁
|
| 777 |
+
let isGalleryFetching = false;
|
| 778 |
+
|
| 779 |
+
async function fetchAndRenderGallery(modelName, triggerAnim = false) {
|
| 780 |
+
// 🌟 核心修复 2:如果已经在请求中,直接锁死,不准并发
|
| 781 |
+
if (isGalleryFetching) {
|
| 782 |
+
console.log("⚠️ 拦截并发请求,当前图库正在加载中...");
|
| 783 |
+
return;
|
| 784 |
+
}
|
| 785 |
+
isGalleryFetching = true;
|
| 786 |
+
|
| 787 |
+
const grid = document.getElementById('gallery-grid');
|
| 788 |
+
const countEl = document.getElementById('lora-count');
|
| 789 |
+
|
| 790 |
+
// 🌟 注意:这里不再清空 grid.innerHTML,因为我们在 unet-select 里已经清空了
|
| 791 |
+
// 但为了首次加载的安全,如果 grid 里没有 Loading 提示,还是补一个
|
| 792 |
+
if (!grid.innerHTML.includes("Loading") && !grid.innerHTML.includes("Switching")) {
|
| 793 |
+
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px; grid-column: span 4;">⏳ Loading character list...</div>`;
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
try {
|
| 797 |
+
const modelInfo = globalModelData[modelName];
|
| 798 |
|
| 799 |
+
if (!modelInfo || !modelInfo.engine) {
|
| 800 |
+
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px; grid-column: span 4;">⏳ Backend data syncing...</div>`;
|
| 801 |
+
return;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
const engineKey = modelInfo.engine;
|
| 805 |
+
// 🌟 别忘了你之前给 getLoras 加了 10 秒超时
|
| 806 |
+
const data = await BackendAPI.getLoras();
|
| 807 |
+
|
| 808 |
+
const lorasDict = data.loras || {};
|
| 809 |
+
const lorasForModel = lorasDict[engineKey];
|
| 810 |
+
|
| 811 |
+
if (!lorasForModel || Object.keys(lorasForModel).length === 0) {
|
| 812 |
+
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px; grid-column: span 4;">No character LoRA for this model</div>`;
|
| 813 |
+
if (countEl) countEl.innerText = "0 Models";
|
| 814 |
+
return;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
currentGalleryData = Object.entries(lorasForModel).map(([filename, info]) => ({
|
| 818 |
+
filename: filename,
|
| 819 |
+
title: filename.replace(".safetensors", ""),
|
| 820 |
+
webp: info.webp
|
| 821 |
+
}));
|
| 822 |
+
|
| 823 |
+
renderedCount = 0;
|
| 824 |
+
shouldAnimateNextRender = triggerAnim;
|
| 825 |
+
|
| 826 |
+
if (countEl) countEl.innerText = `${currentGalleryData.length} Models`;
|
| 827 |
+
|
| 828 |
+
// 🌟 核心:把 flex 居中还原成原来的网格布局
|
| 829 |
+
grid.style.display = 'grid';
|
| 830 |
+
grid.style.height = 'auto';
|
| 831 |
+
|
| 832 |
+
grid.innerHTML = "";
|
| 833 |
+
loadMoreCards();
|
| 834 |
+
|
| 835 |
+
const container = document.getElementById('gallery-container');
|
| 836 |
+
container.onscroll = () => {
|
| 837 |
+
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
|
| 838 |
+
loadMoreCards();
|
| 839 |
+
}
|
| 840 |
+
};
|
| 841 |
+
} catch (error) {
|
| 842 |
+
console.error("Severe error in gallery request:", error);
|
| 843 |
+
grid.innerHTML = `<div style="color: #ff4444; font-size: 13px; grid-column: span 4;">❌ Failed to load gallery, backend may be restarting</div>`;
|
| 844 |
+
if (countEl) countEl.innerText = "Error";
|
| 845 |
+
} finally {
|
| 846 |
+
// 🌟 核心修复 3:无论成功失败,必须释放锁
|
| 847 |
+
isGalleryFetching = false;
|
| 848 |
+
}
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
function applyCardShimmer(card, imgElement) {
|
| 852 |
+
// 🌟 1. 强制卡片裁剪溢出内容 (就地加内联样式,不碰 css 文件)
|
| 853 |
+
card.style.overflow = 'hidden';
|
| 854 |
+
// 确保卡片有相对定位,这样扫光层才能完美贴合
|
| 855 |
+
if (getComputedStyle(card).position === 'static') {
|
| 856 |
+
card.style.position = 'relative';
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
const overlay = document.createElement('div');
|
| 860 |
+
overlay.className = 'shimmer-overlay';
|
| 861 |
+
|
| 862 |
+
// 🌟 2. 给扫光层加绝对定位的内联样式,死死盖在图片上方
|
| 863 |
+
overlay.style.position = 'absolute';
|
| 864 |
+
overlay.style.inset = '0';
|
| 865 |
+
overlay.style.zIndex = '10';
|
| 866 |
+
|
| 867 |
+
// 🌟 3. 加上渐变动画效果
|
| 868 |
+
overlay.style.transition = 'opacity 0.3s ease-out';
|
| 869 |
+
overlay.style.opacity = '1';
|
| 870 |
+
|
| 871 |
+
card.appendChild(overlay);
|
| 872 |
+
|
| 873 |
+
// 🌟 4. 隐藏原图,防止逐行渲染露馅
|
| 874 |
+
imgElement.style.opacity = '0';
|
| 875 |
+
imgElement.style.transition = 'opacity 0.3s ease-out';
|
| 876 |
+
|
| 877 |
+
const revealImage = () => {
|
| 878 |
+
// 等两帧,确保浏览器把布局和尺寸都算准了
|
| 879 |
+
requestAnimationFrame(() => {
|
| 880 |
+
requestAnimationFrame(() => {
|
| 881 |
+
// 让图片显形,同时让扫光层淡出
|
| 882 |
+
imgElement.style.opacity = '1';
|
| 883 |
+
overlay.style.opacity = '0';
|
| 884 |
+
|
| 885 |
+
// 等动画播完后再移除 DOM
|
| 886 |
+
setTimeout(() => {
|
| 887 |
+
if (overlay.parentNode) {
|
| 888 |
+
overlay.remove();
|
| 889 |
+
}
|
| 890 |
+
}, 300);
|
| 891 |
+
});
|
| 892 |
+
});
|
| 893 |
+
};
|
| 894 |
+
|
| 895 |
+
if (imgElement.complete) {
|
| 896 |
+
if (imgElement.decode) {
|
| 897 |
+
imgElement.decode().then(revealImage).catch(revealImage);
|
| 898 |
+
} else {
|
| 899 |
+
revealImage();
|
| 900 |
+
}
|
| 901 |
+
} else {
|
| 902 |
+
imgElement.addEventListener('load', () => {
|
| 903 |
+
if (imgElement.decode) {
|
| 904 |
+
imgElement.decode().then(revealImage).catch(revealImage);
|
| 905 |
+
} else {
|
| 906 |
+
revealImage();
|
| 907 |
+
}
|
| 908 |
+
});
|
| 909 |
+
imgElement.addEventListener('error', () => {
|
| 910 |
+
imgElement.style.opacity = '1'; // 报错也显示占位图
|
| 911 |
+
overlay.remove();
|
| 912 |
+
});
|
| 913 |
+
}
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
function bindCardClick(card, item) {
|
| 917 |
+
card.addEventListener('click', () => {
|
| 918 |
+
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
|
| 919 |
+
card.classList.add('selected');
|
| 920 |
+
selectedLora = item.filename;
|
| 921 |
+
|
| 922 |
+
const nameEl = document.getElementById('selected-lora-name');
|
| 923 |
+
nameEl.innerText = item.title;
|
| 924 |
+
nameEl.style.color = item.title ? 'var(--accent)' : 'var(--text-muted)';
|
| 925 |
+
|
| 926 |
+
const bigImg = document.getElementById('selected-lora-big-img');
|
| 927 |
+
const placeholder = document.getElementById('preview-placeholder');
|
| 928 |
+
|
| 929 |
+
bigImg.style.display = 'none';
|
| 930 |
+
placeholder.style.display = 'flex';
|
| 931 |
+
bigImg.classList.add('shimmer-active');
|
| 932 |
+
bigImg.src = item.webp;
|
| 933 |
+
|
| 934 |
+
bigImg.onload = () => {
|
| 935 |
+
bigImg.style.display = 'block';
|
| 936 |
+
placeholder.style.display = 'none';
|
| 937 |
+
bigImg.onload = null;
|
| 938 |
+
};
|
| 939 |
+
|
| 940 |
+
bigImg.onerror = () => {
|
| 941 |
+
bigImg.style.display = 'none';
|
| 942 |
+
placeholder.style.display = 'flex';
|
| 943 |
+
bigImg.onerror = null;
|
| 944 |
+
};
|
| 945 |
+
|
| 946 |
+
setTimeout(() => {
|
| 947 |
+
bigImg.classList.remove('shimmer-active');
|
| 948 |
+
}, 400);
|
| 949 |
+
});
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
function loadMoreCards() {
|
| 953 |
+
const grid = document.getElementById('gallery-grid');
|
| 954 |
+
const nextBatch = currentGalleryData.slice(renderedCount, renderedCount + BATCH_SIZE);
|
| 955 |
+
if (nextBatch.length === 0) return;
|
| 956 |
+
|
| 957 |
+
// 🌟 注意这里要加 (item, index)
|
| 958 |
+
nextBatch.forEach((item, index) => {
|
| 959 |
+
const card = document.createElement('div');
|
| 960 |
+
card.className = 'lora-card';
|
| 961 |
+
card.dataset.filename = item.filename;
|
| 962 |
+
|
| 963 |
+
// 🌟 动画初始状态
|
| 964 |
+
if (shouldAnimateNextRender) {
|
| 965 |
+
card.style.opacity = '0';
|
| 966 |
+
card.style.transform = 'translateY(20px)';
|
| 967 |
+
card.dataset.animateEntry = "true";
|
| 968 |
+
card.dataset.index = index;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
card.innerHTML = `
|
| 972 |
+
<img src="${item.webp}" loading="lazy" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x400?text=No+Image'">
|
| 973 |
+
<div class="card-info">
|
| 974 |
+
<div class="card-title">${item.title}</div>
|
| 975 |
+
</div>
|
| 976 |
+
`;
|
| 977 |
+
|
| 978 |
+
const cardImg = card.querySelector('img');
|
| 979 |
+
applyCardShimmer(card, cardImg);
|
| 980 |
+
bindCardClick(card, item);
|
| 981 |
+
|
| 982 |
+
grid.appendChild(card);
|
| 983 |
+
|
| 984 |
+
// 🌟 动画执行
|
| 985 |
+
if (shouldAnimateNextRender) {
|
| 986 |
+
// 错开延迟触发���利用你配置里的 staggerDelay (30ms)
|
| 987 |
+
setTimeout(() => {
|
| 988 |
+
applySpringAnimation(card);
|
| 989 |
+
}, index * ANIM_CONFIG.staggerDelay);
|
| 990 |
+
}
|
| 991 |
+
});
|
| 992 |
+
|
| 993 |
+
renderedCount += nextBatch.length;
|
| 994 |
+
|
| 995 |
+
// 🌟 重要:动画只管第一屏。渲染完之后立刻关掉,防止滚动加载时还会闪烁。
|
| 996 |
+
shouldAnimateNextRender = false;
|
| 997 |
+
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
// ==========================================
|
| 1001 |
+
// 🪄 Animation: 物理弹簧动画
|
| 1002 |
+
// ==========================================
|
| 1003 |
+
function applySpringAnimation(element) {
|
| 1004 |
+
if (!element || element.dataset.animating === 'true') return;
|
| 1005 |
+
element.dataset.animating = 'true';
|
| 1006 |
+
|
| 1007 |
+
const startY = ANIM_CONFIG.startY;
|
| 1008 |
+
const omega0 = Math.sqrt(ANIM_CONFIG.stiffness);
|
| 1009 |
+
const zeta = ANIM_CONFIG.damping;
|
| 1010 |
+
const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);
|
| 1011 |
+
const startTime = performance.now();
|
| 1012 |
+
|
| 1013 |
+
element.style.transition = '';
|
| 1014 |
+
element.style.opacity = '0';
|
| 1015 |
+
element.style.transform = `translate3d(0, ${startY}px, 0)`;
|
| 1016 |
+
element.style.willChange = 'transform, opacity';
|
| 1017 |
+
element.style.backfaceVisibility = 'hidden';
|
| 1018 |
+
|
| 1019 |
+
void element.offsetWidth;
|
| 1020 |
+
|
| 1021 |
+
function step(currentTime) {
|
| 1022 |
+
const t = (currentTime - startTime) / 1000;
|
| 1023 |
+
const envelope = Math.exp(-zeta * omega0 * t);
|
| 1024 |
+
|
| 1025 |
+
if (Math.abs(envelope * startY) < 0.1) {
|
| 1026 |
+
// 🌟 核心修复:不要用 translate3d,直接清空 transform,让父级彻底“干干净净”
|
| 1027 |
+
element.style.transform = '';
|
| 1028 |
+
element.style.opacity = 1;
|
| 1029 |
+
element.style.willChange = 'auto';
|
| 1030 |
+
element.style.transition = '';
|
| 1031 |
+
element.dataset.animating = 'false';
|
| 1032 |
+
return;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
const positionY = startY * envelope * Math.cos(omegaD * t);
|
| 1036 |
+
const opacity = Math.min(t / 0.25, 1);
|
| 1037 |
+
|
| 1038 |
+
element.style.transform = `translate3d(0, ${positionY}px, 0)`;
|
| 1039 |
+
element.style.opacity = opacity;
|
| 1040 |
+
|
| 1041 |
+
requestAnimationFrame(step);
|
| 1042 |
+
}
|
| 1043 |
+
|
| 1044 |
+
requestAnimationFrame(step);
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
|
| 1048 |
+
// ==========================================================================
|
| 1049 |
+
// ⚡ Event Listeners (Generate, Switch, Random Seed)
|
| 1050 |
+
// ==========================================================================
|
| 1051 |
+
document.getElementById('random-seed-btn').addEventListener('click', (e) => {
|
| 1052 |
+
e.preventDefault();
|
| 1053 |
+
document.getElementById('seed').value = Math.floor(Math.random() * 4294967295);
|
| 1054 |
+
});
|
| 1055 |
+
|
| 1056 |
+
// --------------------------------------------------------------------------
|
| 1057 |
+
// 📋 Copy Logs Logic
|
| 1058 |
+
// --------------------------------------------------------------------------
|
| 1059 |
+
document.getElementById('copy-log-btn').addEventListener('click', async () => {
|
| 1060 |
+
const logContent = document.getElementById('log-content');
|
| 1061 |
+
// 获取纯文本,排除多余的 DOM 结构
|
| 1062 |
+
const logText = logContent.innerText;
|
| 1063 |
+
|
| 1064 |
+
try {
|
| 1065 |
+
await navigator.clipboard.writeText(logText);
|
| 1066 |
+
// 复制成功反馈,把按钮文字临时改掉
|
| 1067 |
+
const btn = document.getElementById('copy-log-btn');
|
| 1068 |
+
const originalText = btn.innerHTML;
|
| 1069 |
+
btn.innerHTML = '📋 Copied';
|
| 1070 |
+
|
| 1071 |
+
setTimeout(() => {
|
| 1072 |
+
btn.innerHTML = originalText;
|
| 1073 |
+
btn.style.color = 'var(--text-muted)';
|
| 1074 |
+
}, 2000);
|
| 1075 |
+
|
| 1076 |
+
} catch (err) {
|
| 1077 |
+
console.error('Failed to copy logs:', err);
|
| 1078 |
+
alert('Failed to copy logs.');
|
| 1079 |
+
}
|
| 1080 |
+
});
|
| 1081 |
+
|
| 1082 |
+
document.getElementById('unet-select').addEventListener('change', async (e) => {
|
| 1083 |
+
const selectedModel = e.target.value;
|
| 1084 |
+
const currentEngine = globalModelData[selectedModel]?.engine || "unknown";
|
| 1085 |
+
const lastEngine = window._lastUsedArchitecture || null;
|
| 1086 |
+
const isCrossArchitecture = (lastEngine !== null && lastEngine !== currentEngine);
|
| 1087 |
+
|
| 1088 |
+
// 🌟 新增:更新 TE 和 VAE 的展示
|
| 1089 |
+
const modelInfo = globalModelData[selectedModel];
|
| 1090 |
+
const infoEl = document.getElementById('model-assets-info');
|
| 1091 |
+
|
| 1092 |
+
if (modelInfo && infoEl) {
|
| 1093 |
+
const te = modelInfo.te_name || "Unknown";
|
| 1094 |
+
const vae = modelInfo.vae_name || "Unknown";
|
| 1095 |
+
infoEl.innerHTML = `TE: ${te}<br>VAE: ${vae}`;
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
|
| 1099 |
+
// 🌟 核心修复 1:如果是跨架构切换,第一时间强杀旧 DOM,别等网络请求
|
| 1100 |
+
if (isCrossArchitecture) {
|
| 1101 |
+
const grid = document.getElementById('gallery-grid');
|
| 1102 |
+
|
| 1103 |
+
// 🌟 核心:强行把 grid 改成 flex 居中
|
| 1104 |
+
grid.style.display = 'flex';
|
| 1105 |
+
grid.style.justifyContent = 'center';
|
| 1106 |
+
grid.style.alignItems = 'center';
|
| 1107 |
+
grid.style.height = '100%'; // 撑满父容器高度
|
| 1108 |
+
|
| 1109 |
+
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px;">⏳ Loading LoRAs...</div>`;
|
| 1110 |
+
|
| 1111 |
+
document.getElementById('lora-count').innerText = "Loading...";
|
| 1112 |
+
currentGalleryData = [];
|
| 1113 |
+
renderedCount = 0;
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
// 1. 处理 Prompt 覆盖
|
| 1117 |
+
const promptEl = document.getElementById('prompt');
|
| 1118 |
+
const lastModelName = window._currentSelectedModel || null;
|
| 1119 |
+
const previousDefaultPrompt = lastModelName ? (globalModelData[lastModelName]?.default_prompt || "") : "";
|
| 1120 |
+
|
| 1121 |
+
if (!promptEl.value.trim() || promptEl.value === previousDefaultPrompt) {
|
| 1122 |
+
if (globalModelData[selectedModel]?.default_prompt) {
|
| 1123 |
+
promptEl.value = globalModelData[selectedModel].default_prompt;
|
| 1124 |
+
}
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
// 2. 处理参数更新 (不管跨不跨架构,参数肯定要更新)
|
| 1128 |
+
const modelDefaults = globalModelData[selectedModel]?.defaults || {};
|
| 1129 |
+
|
| 1130 |
+
if (modelDefaults.cfg !== undefined) {
|
| 1131 |
+
document.getElementById('cfg-slider').value = modelDefaults.cfg;
|
| 1132 |
+
document.getElementById('cfg-input').value = Number(modelDefaults.cfg).toFixed(1);
|
| 1133 |
+
}
|
| 1134 |
+
if (modelDefaults.steps !== undefined) {
|
| 1135 |
+
document.getElementById('steps-slider').value = modelDefaults.steps;
|
| 1136 |
+
document.getElementById('steps-input').value = modelDefaults.steps;
|
| 1137 |
+
}
|
| 1138 |
+
if (modelDefaults.sampler !== undefined) {
|
| 1139 |
+
document.getElementById('sampler').value = modelDefaults.sampler;
|
| 1140 |
+
}
|
| 1141 |
+
if (modelDefaults.scheduler !== undefined) {
|
| 1142 |
+
document.getElementById('scheduler').value = modelDefaults.scheduler;
|
| 1143 |
+
}
|
| 1144 |
+
if (modelDefaults.char_lora_strength !== undefined && (isCrossArchitecture || !selectedLora)) {
|
| 1145 |
+
document.getElementById('char-strength').value = Number(modelDefaults.char_lora_strength).toFixed(1);
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
// ========================================================
|
| 1149 |
+
// 🌟 核心分支:只有跨架构才执行渲染,同架构直接躺平
|
| 1150 |
+
// ========================================================
|
| 1151 |
+
if (isCrossArchitecture) {
|
| 1152 |
+
// 跨架构:清空已选角色,换 Base LoRA,重新拉数据(并传 true 触发入场动画)
|
| 1153 |
+
selectedLora = "";
|
| 1154 |
+
document.getElementById('selected-lora-name').innerText = "None";
|
| 1155 |
+
document.getElementById('selected-lora-name').style.color = 'var(--text-muted)';
|
| 1156 |
+
|
| 1157 |
+
const bigImg = document.getElementById('selected-lora-big-img');
|
| 1158 |
+
const placeholder = document.getElementById('preview-placeholder');
|
| 1159 |
+
bigImg.src = "";
|
| 1160 |
+
bigImg.style.display = 'none';
|
| 1161 |
+
placeholder.style.display = 'flex';
|
| 1162 |
+
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
|
| 1163 |
+
|
| 1164 |
+
renderBaseLoras(selectedModel);
|
| 1165 |
+
fetchAndRenderGallery(selectedModel, true); // 👈 后台异步加载,不阻塞事件
|
| 1166 |
+
|
| 1167 |
+
// 强制滚动到图库顶部
|
| 1168 |
+
const galleryContainer = document.getElementById('gallery-container');
|
| 1169 |
+
if (galleryContainer) galleryContainer.scrollTop = 0;
|
| 1170 |
+
|
| 1171 |
+
} else {
|
| 1172 |
+
// 🌟 同架构:不请求网络,不动画廊,完美保留 DOM 和滚动位置
|
| 1173 |
+
console.log("🔄 Same architecture, keeping gallery and Base LoRAs intact.");
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
// 更新全局记录
|
| 1177 |
+
window._currentSelectedModel = selectedModel;
|
| 1178 |
+
window._lastUsedArchitecture = currentEngine;
|
| 1179 |
+
});
|
| 1180 |
+
|
| 1181 |
+
document.getElementById('generate-btn').addEventListener('click', async () => {
|
| 1182 |
+
if (!backendState.isReady) {
|
| 1183 |
+
alert("Backend is waking up. Please wait a few seconds and try again!");
|
| 1184 |
+
return;
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
const resultImage = document.getElementById('result-image');
|
| 1188 |
+
const placeholderText = document.getElementById('placeholder-text');
|
| 1189 |
+
const generateBtn = document.getElementById('generate-btn');
|
| 1190 |
+
|
| 1191 |
+
const prompt = document.getElementById('prompt').value.trim();
|
| 1192 |
+
const unet_choice = document.getElementById('unet-select').value;
|
| 1193 |
+
const custom_w = parseInt(document.getElementById('width-input').value);
|
| 1194 |
+
const custom_h = parseInt(document.getElementById('height-input').value);
|
| 1195 |
+
const steps = parseInt(document.getElementById('steps-input').value);
|
| 1196 |
+
const cfg = parseFloat(document.getElementById('cfg-input').value);
|
| 1197 |
+
const sampler = document.getElementById('sampler').value;
|
| 1198 |
+
const scheduler = document.getElementById('scheduler').value;
|
| 1199 |
+
|
| 1200 |
+
const seedVal = document.getElementById('seed').value;
|
| 1201 |
+
const seed = seedVal === "" ? -1 : parseInt(seedVal);
|
| 1202 |
+
|
| 1203 |
+
if (!unet_choice || unet_choice.includes('Loading') || unet_choice.includes('Backend Offline')) {
|
| 1204 |
+
alert("Please select a valid Base Model first."); return;
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
if (!prompt) {
|
| 1208 |
+
alert("Please enter a prompt.");
|
| 1209 |
+
return;
|
| 1210 |
+
}
|
| 1211 |
+
|
| 1212 |
+
const baseLoraStates = [];
|
| 1213 |
+
document.querySelectorAll('#base-lora-container .lora-row').forEach(row => {
|
| 1214 |
+
const isChecked = row.querySelector('.base-lora-cb').checked;
|
| 1215 |
+
const strength = parseFloat(row.querySelector('.base-lora-strength').value) || 1.0;
|
| 1216 |
+
baseLoraStates.push(isChecked, strength);
|
| 1217 |
+
});
|
| 1218 |
+
|
| 1219 |
+
const charEnable = document.getElementById('char-enable').checked;
|
| 1220 |
+
const charStrength = parseFloat(document.getElementById('char-strength').value) || 1.0;
|
| 1221 |
+
|
| 1222 |
+
const requestData = {
|
| 1223 |
+
prompt: prompt,
|
| 1224 |
+
unet_choice: unet_choice,
|
| 1225 |
+
custom_w: custom_w,
|
| 1226 |
+
custom_h: custom_h,
|
| 1227 |
+
steps: steps,
|
| 1228 |
+
cfg: cfg,
|
| 1229 |
+
seed: seed,
|
| 1230 |
+
sampler: sampler,
|
| 1231 |
+
scheduler: scheduler,
|
| 1232 |
+
char_enable: charEnable,
|
| 1233 |
+
char_lora: selectedLora || "",
|
| 1234 |
+
char_strength: charStrength,
|
| 1235 |
+
base_lora_states: baseLoraStates
|
| 1236 |
+
};
|
| 1237 |
+
|
| 1238 |
+
console.log("🚀 Submitting parameters:", requestData);
|
| 1239 |
+
|
| 1240 |
+
if (!requestData.prompt || !requestData.unet_choice || !requestData.custom_w) {
|
| 1241 |
+
alert("Missing parameters. Please check Prompt / Model / Dimensions.");
|
| 1242 |
+
return;
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
generateBtn.disabled = true;
|
| 1246 |
+
generateBtn.innerText = "⏳ Submitting...";
|
| 1247 |
+
resultImage.style.display = 'none';
|
| 1248 |
+
placeholderText.style.display = 'block';
|
| 1249 |
+
|
| 1250 |
+
generateStartTime = Date.now();
|
| 1251 |
+
currentStageStartTime = generateStartTime;
|
| 1252 |
+
currentStageName = "Task Queued";
|
| 1253 |
+
|
| 1254 |
+
document.getElementById('log-content').innerHTML = "";
|
| 1255 |
+
|
| 1256 |
+
if (window._lastGeneratedModel !== unet_choice) {
|
| 1257 |
+
const currentEngine = globalModelData[unet_choice]?.engine || "unknown";
|
| 1258 |
+
const lastModel = window._lastGeneratedModel;
|
| 1259 |
+
const lastEngine = lastModel ? (globalModelData[lastModel]?.engine || "unknown") : null;
|
| 1260 |
+
|
| 1261 |
+
if (lastEngine && lastEngine !== currentEngine) {
|
| 1262 |
+
addLog(`🔄 Arch Switch: Loading UNet/VAE/TE (Full Swap)...`, "highlight");
|
| 1263 |
+
} else if (lastEngine === currentEngine) {
|
| 1264 |
+
addLog(`🔄 Model Update: Swapping UNet (Keep VAE/TE)...`, "highlight");
|
| 1265 |
+
} else {
|
| 1266 |
+
addLog(`🔄 Initializing: Loading UNet/VAE/TE (First Load)...`, "highlight");
|
| 1267 |
+
}
|
| 1268 |
+
window._lastGeneratedModel = unet_choice;
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
addLog("Task submitted to backend.", "highlight");
|
| 1272 |
+
|
| 1273 |
+
try {
|
| 1274 |
+
const taskId = await BackendAPI.submitGenerate(requestData);
|
| 1275 |
+
addLog(`Task ID: ${taskId}`);
|
| 1276 |
+
|
| 1277 |
+
BackendAPI.pollStatus(taskId, {
|
| 1278 |
+
onProcessing: (msg) => {
|
| 1279 |
+
let safeMsg = msg || "🎨 Processing...";
|
| 1280 |
+
let newStage = "Processing";
|
| 1281 |
+
|
| 1282 |
+
if (safeMsg.includes("Rendering") || safeMsg.includes("steps") || safeMsg.includes("Sampling") || safeMsg.includes("KSampler")) {
|
| 1283 |
+
newStage = "Comfy Backend - Sampling";
|
| 1284 |
+
}
|
| 1285 |
+
else if (safeMsg.includes("Downloading")) {
|
| 1286 |
+
newStage = "Downloading";
|
| 1287 |
}
|
| 1288 |
+
else if (safeMsg.includes("Requesting") || safeMsg.includes("Waiting") || safeMsg.includes("Queue")) {
|
| 1289 |
+
newStage = "Queueing";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
}
|
| 1291 |
+
else if (safeMsg.includes("Allocating") || safeMsg.includes("GPU Acquired") || safeMsg.includes("Loading") || safeMsg.includes("Executing") || safeMsg.includes("Workflow")) {
|
| 1292 |
+
newStage = "Comfy Backend - Model Loading";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
}
|
| 1294 |
+
|
| 1295 |
+
if ((currentStageName === "Comfy Backend - Model Loading" || currentStageName === "Comfy Backend - Sampling")
|
| 1296 |
+
&& (safeMsg.includes("Requesting") || safeMsg.includes("Waiting") || safeMsg.includes("Queue"))) {
|
| 1297 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
}
|
| 1299 |
+
|
| 1300 |
+
if (newStage !== currentStageName && newStage !== "Processing") {
|
| 1301 |
+
const now = Date.now();
|
| 1302 |
+
const prevDuration = ((now - currentStageStartTime) / 1000).toFixed(1);
|
| 1303 |
+
|
| 1304 |
+
if (currentStageName !== "Task Queued" && currentStageName !== "Processing") {
|
| 1305 |
+
addLog(`⏱️ ${currentStageName} finished in ${prevDuration}s.`);
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
currentStageName = newStage;
|
| 1309 |
+
currentStageStartTime = now;
|
| 1310 |
+
|
| 1311 |
+
addLog(`🚀 Starting ${newStage}...`);
|
| 1312 |
}
|
| 1313 |
+
|
| 1314 |
+
const logContent = document.getElementById('log-content');
|
| 1315 |
+
const lastLog = logContent.lastElementChild;
|
| 1316 |
+
const lastText = lastLog ? lastLog.innerText : "";
|
| 1317 |
+
|
| 1318 |
+
if (safeMsg.includes("Rendering image (GPU processing)") && !safeMsg.includes("steps")) {
|
| 1319 |
+
const elapsed = ((Date.now() - currentStageStartTime) / 1000).toFixed(0);
|
| 1320 |
+
|
| 1321 |
+
const lastLine = logContent.lastElementChild ? logContent.lastElementChild.innerText : "";
|
| 1322 |
+
const lastElapsedMatch = lastLine.match(/\((\d+)s\)/);
|
| 1323 |
+
|
| 1324 |
+
if (!lastLine.includes("Rendering image") || !lastElapsedMatch || parseInt(lastElapsedMatch[1]) !== parseInt(elapsed)) {
|
| 1325 |
+
addLog(`🎨 Rendering image (GPU processing)... (${elapsed}s)`);
|
| 1326 |
+
}
|
| 1327 |
+
smartScroll();
|
| 1328 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
}
|
| 1330 |
+
|
| 1331 |
+
if (safeMsg.includes("steps")) {
|
| 1332 |
+
if (currentStageName !== "Comfy Backend - Sampling") {
|
| 1333 |
+
const now = Date.now();
|
| 1334 |
+
const prevDuration = ((now - currentStageStartTime) / 1000).toFixed(1);
|
| 1335 |
+
|
| 1336 |
+
if (currentStageName === "Comfy Backend - Model Loading") {
|
| 1337 |
+
addLog(`⏱️ Comfy Backend - Model Loading finished in ${prevDuration}s.`, "normal");
|
| 1338 |
+
addLog(`🚀 Starting Comfy Backend - Sampling...`, "normal");
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
currentStageName = "Comfy Backend - Sampling";
|
| 1342 |
+
currentStageStartTime = now;
|
| 1343 |
+
}
|
| 1344 |
+
|
| 1345 |
+
const lastLine = logContent.lastElementChild;
|
| 1346 |
+
if (!lastLine || !lastLine.innerText.includes(safeMsg)) {
|
| 1347 |
+
addLog(`\u2003\u2003${safeMsg}`, "normal");
|
| 1348 |
+
}
|
| 1349 |
+
smartScroll();
|
| 1350 |
+
return;
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
if (currentStageName === "Loading VRAM" && !safeMsg.includes("Executing") && (safeMsg.includes("Waiting for GPU") || safeMsg.includes("Queue for GPU"))) {
|
| 1354 |
+
return;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
if (safeMsg.includes("Loading") || safeMsg.includes("Swapping") || safeMsg.includes("Waiting") || safeMsg.includes("GPU Acquired") || safeMsg.includes("Queue") || safeMsg.includes("Executing") || safeMsg.includes("Sampling") || safeMsg.includes("KSampler") || currentStageName === "Comfy Backend - Sampling") {
|
| 1358 |
+
const elapsed = ((Date.now() - currentStageStartTime) / 1000).toFixed(0);
|
| 1359 |
+
|
| 1360 |
+
const lastLine = logContent.lastElementChild;
|
| 1361 |
+
let isNewSecond = true;
|
| 1362 |
+
if (lastLine && lastLine.dataset.lastElapsed === elapsed) {
|
| 1363 |
+
isNewSecond = false;
|
| 1364 |
+
}
|
| 1365 |
+
|
| 1366 |
+
if (isNewSecond) {
|
| 1367 |
+
const newLine = document.createElement('div');
|
| 1368 |
+
newLine.className = 'log-line';
|
| 1369 |
+
|
| 1370 |
+
let displayMsg = safeMsg;
|
| 1371 |
+
if (currentStageName === "Comfy Backend - Model Loading") {
|
| 1372 |
+
displayMsg = "📦 Loading tensors to VRAM...";
|
| 1373 |
+
} else if (currentStageName === "Comfy Backend - Sampling") {
|
| 1374 |
+
displayMsg = "🎨 Running KSampler...";
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
newLine.innerText = `[${new Date().toLocaleTimeString('en-US', { hour12: false })}] ${displayMsg} (Elapsed: ${elapsed}s)`;
|
| 1378 |
+
newLine.dataset.lastElapsed = elapsed;
|
| 1379 |
+
|
| 1380 |
+
logContent.appendChild(newLine);
|
| 1381 |
+
if (logContent.childElementCount > 100) {
|
| 1382 |
+
logContent.firstElementChild.remove();
|
| 1383 |
+
}
|
| 1384 |
+
}
|
| 1385 |
+
}
|
| 1386 |
+
else {
|
| 1387 |
+
if (!lastText.includes(safeMsg)) {
|
| 1388 |
+
addLog(safeMsg);
|
| 1389 |
+
}
|
| 1390 |
}
|
| 1391 |
+
|
| 1392 |
+
smartScroll()
|
| 1393 |
},
|
| 1394 |
+
onSuccess: async (imageBase64, returnedSeed, metrics, filename) => {
|
| 1395 |
+
placeholderText.style.display = 'none';
|
| 1396 |
+
console.log("🚨 DEBUG METRICS:", JSON.stringify(metrics));
|
| 1397 |
+
|
| 1398 |
+
// 🌟 新增:打印后端传过来的文件名
|
| 1399 |
+
console.log("✅ 后端返回的正确文件名:", filename);
|
| 1400 |
+
|
| 1401 |
+
const now = Date.now();
|
| 1402 |
+
const totalDuration = ((now - generateStartTime) / 1000).toFixed(1);
|
| 1403 |
+
|
| 1404 |
+
const nowForStage = Date.now();
|
| 1405 |
+
const prevStageDuration = ((nowForStage - currentStageStartTime) / 1000).toFixed(1);
|
| 1406 |
+
|
| 1407 |
+
if (currentStageName === "Comfy Backend - Model Loading") {
|
| 1408 |
+
addLog(`⏱️ Comfy Backend - Model Loading finished in ${prevStageDuration}s.`, "normal");
|
| 1409 |
+
addLog(`🚀 Starting Comfy Backend - Sampling...`, "normal");
|
| 1410 |
+
|
| 1411 |
+
const realSteps = metrics?.steps || "?";
|
| 1412 |
+
addLog(`🎨 Rendering: ${realSteps}/${realSteps} steps (100%)`, "normal");
|
| 1413 |
+
|
| 1414 |
+
const pureRender = metrics ? (metrics.workflow - metrics.load_time) : 0;
|
| 1415 |
+
addLog(`⏱️ Comfy Backend - Sampling finished in ${pureRender > 0 ? pureRender.toFixed(2) : "0.0"}s.`, "normal");
|
| 1416 |
+
} else if (currentStageName === "Comfy Backend - Sampling") {
|
| 1417 |
+
addLog(`⏱️ Comfy Backend - Sampling finished in ${prevStageDuration}s.`, "normal");
|
| 1418 |
+
} else if (currentStageName === "Queueing" || currentStageName === "Comfy Workflow") {
|
| 1419 |
+
addLog(`⏱️ Task execution completed. Syncing final stages...`, "normal");
|
| 1420 |
}
|
| 1421 |
+
|
| 1422 |
+
addLog("✅ Generation successful!", "highlight");
|
| 1423 |
+
addLog(`⏱️ Total Wait Time: ${totalDuration}s`, "highlight");
|
| 1424 |
+
|
| 1425 |
+
if (metrics) {
|
| 1426 |
+
addLog(`----------------------------------------`, "normal");
|
| 1427 |
+
addLog(`📊 [Backend Precise Metrics]`, "highlight");
|
| 1428 |
+
|
| 1429 |
+
const dl = Number(metrics.download || 0);
|
| 1430 |
+
const q = Number(metrics.queue || 0);
|
| 1431 |
+
const wf = Number(metrics.workflow || 0);
|
| 1432 |
+
const load = Number(metrics.load_time || 0);
|
| 1433 |
+
|
| 1434 |
+
const pureRender = wf - load > 0 ? (wf - load) : wf;
|
| 1435 |
+
const serverComputeTotal = (dl + q + wf).toFixed(2);
|
| 1436 |
+
const networkOverhead = Math.max(0, totalDuration - serverComputeTotal).toFixed(1);
|
| 1437 |
+
const secPerStep = pureRender > 0 && metrics.steps > 0 ? (pureRender / metrics.steps).toFixed(2) : 0;
|
| 1438 |
+
|
| 1439 |
+
const metricsLines = [
|
| 1440 |
+
`\u2003\u2003⬇️ Resource Download: ${dl.toFixed(2)}s`,
|
| 1441 |
+
`\u2003\u2003⏳ GPU Queue: ${q.toFixed(2)}s`,
|
| 1442 |
+
load > 0 ? `\u2003\u2003📦 Model Loading: ${load.toFixed(2)}s` : null,
|
| 1443 |
+
`\u2003\u2003🎨 Pure Compute (Sampler & Decode): ${pureRender.toFixed(2)}s (${metrics.steps} steps)`,
|
| 1444 |
+
secPerStep > 0 ? `\u2003\u2003🚀 Average Speed: ${secPerStep}s / step` : null,
|
| 1445 |
+
`\u2003\u2003🌐 Network Overhead: ~${networkOverhead}s`,
|
| 1446 |
+
`\u2003\u2003⚙️ Server Compute Total: ${serverComputeTotal}s`
|
| 1447 |
+
].filter(Boolean);
|
| 1448 |
+
|
| 1449 |
+
// 启动“打字机”
|
| 1450 |
+
let currentIndex = 0;
|
| 1451 |
+
const typeMetrics = () => {
|
| 1452 |
+
if (currentIndex < metricsLines.length) {
|
| 1453 |
+
requestAnimationFrame(() => {
|
| 1454 |
+
addLog(metricsLines[currentIndex], "normal");
|
| 1455 |
+
smartScroll(); // 🌟 强制触发一次滚动检查
|
| 1456 |
+
currentIndex++;
|
| 1457 |
+
setTimeout(typeMetrics, 120);
|
| 1458 |
+
});
|
| 1459 |
+
} else {
|
| 1460 |
+
setTimeout(() => {
|
| 1461 |
+
addLog(`----------------------------------------`, "normal");
|
| 1462 |
+
smartScroll(); // 🌟 收尾也滚一下
|
| 1463 |
+
if (returnedSeed !== undefined && returnedSeed !== null) {
|
| 1464 |
+
addLog(`🎲 Random Seed resolved to: ${returnedSeed}`, "highlight");
|
| 1465 |
+
smartScroll(); // 🌟 Seed 出来也滚一下
|
| 1466 |
+
}
|
| 1467 |
+
}, 300);
|
| 1468 |
+
}
|
| 1469 |
+
};
|
| 1470 |
+
typeMetrics();
|
| 1471 |
+
|
| 1472 |
+
} else {
|
| 1473 |
+
addLog(`⚠️ [Backend Metrics Missing]`, "error");
|
| 1474 |
+
}
|
| 1475 |
+
|
| 1476 |
+
// ==========================================
|
| 1477 |
+
// 🌟 第三阶段:图像渲染与收尾(极简模式)
|
| 1478 |
+
// ==========================================
|
| 1479 |
+
setTimeout(() => {
|
| 1480 |
+
resultImage.onload = () => {
|
| 1481 |
+
resultImage.style.display = 'block';
|
| 1482 |
+
setUIState(false);
|
| 1483 |
+
resultImage.onload = null;
|
| 1484 |
+
};
|
| 1485 |
+
|
| 1486 |
+
// 🌟 核心:因为是 URL 了,加个时间戳防止浏览器缓存旧图
|
| 1487 |
+
if (imageBase64.startsWith('data:')) {
|
| 1488 |
+
resultImage.src = imageBase64;
|
| 1489 |
+
} else {
|
| 1490 |
+
resultImage.src = `${imageBase64}?t=${Date.now()}`;
|
| 1491 |
+
}
|
| 1492 |
+
}, 100);
|
| 1493 |
},
|
| 1494 |
+
onError: (msg) => {
|
| 1495 |
+
let shortMsg = msg;
|
| 1496 |
+
if (msg && msg.length > 60) {
|
| 1497 |
+
shortMsg = msg.substring(0, 60) + "...";
|
|
|
|
| 1498 |
}
|
| 1499 |
+
|
| 1500 |
+
placeholderText.style.display = 'block';
|
| 1501 |
+
placeholderText.innerText = `❌ Failed: ${shortMsg}`;
|
| 1502 |
+
|
| 1503 |
+
setUIState(false);
|
| 1504 |
+
addLog(`❌ Error: ${msg}`, "error");
|
| 1505 |
}
|
| 1506 |
+
});
|
| 1507 |
+
} catch (error) {
|
| 1508 |
+
placeholderText.style.display = 'block';
|
| 1509 |
+
placeholderText.innerText = `❌ Request failed: ${error.message}`;
|
| 1510 |
+
setUIState(false);
|
| 1511 |
+
addLog(`❌ Request Error: ${error.message}`, "error");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1512 |
}
|
| 1513 |
+
});
|
| 1514 |
|
| 1515 |
+
// ==========================================================================
|
| 1516 |
+
// 🚀 Initialization (带双重执行拦截锁)
|
| 1517 |
+
// ==========================================================================
|
| 1518 |
+
let isAppInitialized = false; // 🌟 全局锁
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1519 |
|
| 1520 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
| 1521 |
+
// 🌟 核心修复:如果已经初始化过了,直接 return,拦截掉第二次的幽灵执行
|
| 1522 |
+
|
| 1523 |
+
if (isAppInitialized) {
|
| 1524 |
+
console.log("⚠️ [Init] 拦截到重复的 DOMContentLoaded,已忽略。");
|
| 1525 |
+
return;
|
| 1526 |
+
}
|
| 1527 |
+
isAppInitialized = true;
|
| 1528 |
+
|
| 1529 |
+
|
| 1530 |
+
setTimeout(() => {
|
| 1531 |
+
window._isDataReady = true; // 10秒后强行标记为就绪
|
| 1532 |
+
}, 10000);
|
| 1533 |
+
|
| 1534 |
+
|
| 1535 |
+
console.log("🔥 触发初始化...");
|
| 1536 |
+
setUIState(false);
|
| 1537 |
+
|
| 1538 |
+
try {
|
| 1539 |
+
const isHealthy = await BackendAPI.checkHealth();
|
| 1540 |
+
|
| 1541 |
+
if (isHealthy) {
|
| 1542 |
+
console.log("✅ 后端在线,开始拉取数据");
|
| 1543 |
+
backendState.isReady = true;
|
| 1544 |
+
document.getElementById('backend-status-dot').className = "status-dot online";
|
| 1545 |
+
setUIState(false);
|
| 1546 |
+
await loadInitialData();
|
| 1547 |
+
} else {
|
| 1548 |
+
console.warn("⚠️ 首次心跳失败,启动后台轮询...");
|
| 1549 |
+
document.getElementById('backend-status-dot').className = "status-dot offline";
|
| 1550 |
+
startHealthPolling();
|
| 1551 |
+
await loadInitialData();
|
| 1552 |
+
}
|
| 1553 |
+
} catch (error) {
|
| 1554 |
+
console.error("❌ 初始化发生严重错误:", error);
|
| 1555 |
+
document.getElementById('backend-status-dot').className = "status-dot offline";
|
| 1556 |
+
startHealthPolling();
|
| 1557 |
+
}
|
| 1558 |
+
});
|