yamiuchi commited on
Commit
14fc583
·
verified ·
1 Parent(s): aceb784

Upload validation app: cleaned prompt macros and public aggregate statistics

Browse files
Files changed (49) hide show
  1. .gitattributes +35 -35
  2. .gitignore +19 -0
  3. README.md +331 -6
  4. admin_stats.py +119 -0
  5. app.py +1677 -0
  6. assets/jujo-browser/browser.css +71 -0
  7. assets/jujo-browser/browser.html +24 -0
  8. assets/jujo-browser/browser.js +195 -0
  9. assets/jujo/anima-LICENSE.md +54 -0
  10. assets/jujo/anima-NOTICE.md +38 -0
  11. assets/jujo/anima.json +791 -0
  12. assets/jujo/illustrious-LICENSE +8 -0
  13. assets/jujo/illustrious.json +1783 -0
  14. assets/settings/settings.css +15 -0
  15. assets/settings/settings.html +39 -0
  16. assets/settings/settings.js +203 -0
  17. browser_settings.py +149 -0
  18. generation_stats.py +124 -0
  19. jujo_browser.py +159 -0
  20. jujo_loras.py +94 -0
  21. lora_ui.css +12 -0
  22. requirements.txt +32 -0
  23. scripts/update_jujo_catalog.py +292 -0
  24. tag_completion.py +24 -0
  25. tags/LICENSE.tagcomplete +21 -0
  26. tags/README.md +76 -0
  27. tags/completion.css +30 -0
  28. tags/completion.html +17 -0
  29. tags/completion.js +228 -0
  30. tags/danbooru.csv +0 -0
  31. tags/extra-quality-tags.csv +6 -0
  32. tests/admin_stats_ui.cjs +26 -0
  33. tests/admin_stats_ui_demo.py +27 -0
  34. tests/browser_settings_ui.cjs +168 -0
  35. tests/eye_type_samples_ui.cjs +75 -0
  36. tests/eyecolle_samples_ui.cjs +58 -0
  37. tests/favorites_ui.cjs +137 -0
  38. tests/jujo_browser_ui.cjs +109 -0
  39. tests/lora_ui_browser.cjs +2 -0
  40. tests/lora_ui_demo.py +54 -0
  41. tests/pony_loras_ui.cjs +35 -0
  42. tests/tag_completion_browser.cjs +125 -0
  43. tests/tag_completion_demo.py +29 -0
  44. tests/test_admin_stats.py +90 -0
  45. tests/test_branding.py +26 -0
  46. tests/test_browser_settings.py +69 -0
  47. tests/test_generation_stats.py +128 -0
  48. tests/test_jujo_browser.py +115 -0
  49. tests/test_jujo_loras.py +85 -0
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ComfyUI backend cloned at runtime
2
+ ComfyUI/
3
+
4
+ # models are downloaded at runtime, never commit them
5
+ *.safetensors
6
+ *.gguf
7
+
8
+ # huggingface_hub staging cache
9
+ hf_cache/
10
+
11
+ __pycache__/
12
+ *.pyc
13
+
14
+ # Gradio-generated typing cache for the HTML sample browser
15
+ jujo_browser.pyi
16
+ browser_settings.pyi
17
+
18
+ # Local statistics test / development data (never public)
19
+ generation-stats/
README.md CHANGED
@@ -1,13 +1,338 @@
1
  ---
2
- title: Anima Collection Mcp
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.30.0
8
  python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Anima Collection+ MCP
3
+ emoji: 📚
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.23.1
8
  python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
+ hf_oauth: true
12
+ hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
+ # Anima Collection+ MCP
16
+
17
+ Gradio + ComfyUI text-to-image Space with model-family-isolated pages and an
18
+ MCP-compatible public generation endpoint.
19
+
20
+ Anima Collectionをベースに、まずMCPサーバ機能を追加し、その後Illustrious対応・
21
+ 複数モデル対応・お気に入りなどを拡張したフォークです。
22
+ 「+」は元のAnima Collectionからの機能拡張を表します。
23
+ この検証版のSpace URLは `yamiuchi/anima-collection-plus-mcp` です。
24
+ 元のSpaceとは別の接続先です。動作確認後はPause状態で保留します。
25
+
26
+ ## WebUI pages
27
+
28
+ - **Anima** — Anima-only checkpoints and LoRAs. The WebUI provides three
29
+ isolated presets: official Turbo (8 steps, CFG 1, Turbo LoRA disabled),
30
+ non-Turbo + Turbo LoRA (8 steps, CFG 1, experimental), and standard
31
+ non-Turbo (30 steps, CFG 4, Turbo LoRA disabled).
32
+ - **Illustrious** — Illustrious XL v2.0 checkpoint, CLIP skip 2, and an isolated
33
+ LoRA list. WAI-illustrious defaults are 30 steps, CFG 5, Euler a / `normal`,
34
+ and WAI's `masterpiece, best quality, amazing quality` prompt tags.
35
+ The WebUI separates WAI standard and DMD2 fast presets; DMD2 uses 8 steps,
36
+ CFG 1.6, LCM / `normal`, and LoRA 4 strength 0.7. Its VAE selector defaults
37
+ to the checkpoint-embedded VAE and can switch to Stability AI's SDXL VAE
38
+ or the Illustrious XL v1.0 VAE.
39
+ - **Krea2** — reserved family page with its natural-language and 8-step Turbo
40
+ defaults. Generation stays disabled until Krea2 weights and its Qwen3-VL
41
+ text encoder are explicitly added to this Space.
42
+
43
+ Checkpoint and LoRA candidates are never shared between family pages.
44
+
45
+ ## LoRA selection and JujoHotaru collection
46
+
47
+ The highlighted LoRA accordion provides four optional slots. `---` means no
48
+ selection and is always skipped during generation, even if Enable is submitted
49
+ as true through the API. Anima LoRA 3 and Illustrious LoRA 2/3 start with `---`
50
+ and Enable off.
51
+
52
+ Anima LoRA 1 (P3 Aesthetics Details) and LoRA 2 (GPT-Image Anima Base) also start
53
+ disabled, with their filenames selected and strengths kept at 0.8. Users can
54
+ enable both slots manually to use the previous default LoRA configuration.
55
+ The temporary initial-settings-change announcement has been removed.
56
+
57
+ The author-published [JujoHotaru Anima collection](https://huggingface.co/JujoHotaru/anima_lora)
58
+ (87 files) and [SDXL collection](https://huggingface.co/JujoHotaru/lora/blob/main/sdxl/README.md)
59
+ (188 SDXL files: 171 general/Illustrious and 17 Pony-targeted) are registered separately. Pony releases appear last, labelled `Pony用・ILL未検証`, with compatibility warnings in the selector and sample details. Their author samples do not guarantee reproduction on Illustrious. SD1.5 and superseded versions remain excluded. Existing
60
+ LoRAs remain at the top of each list; author LoRAs show series names and filenames.
61
+ The initially collapsed sample browser sits directly below LoRA settings and above
62
+ Generate on both PC and mobile, and provides category/search
63
+ filters and paginated author-hosted thumbnails (8 per page on PC, 4 on mobile).
64
+ PC shows details alongside the list when space permits, or below it in a narrow column; mobile opens a detail dialog and restores
65
+ the list position when it closes. Tap/click the preview image to enlarge it.
66
+ Some images compare a whole series rather than one file.
67
+ For Eye Collection XL, all 81 code entries instead show the author's individual
68
+ Illustrious example matching that exact code (not the adjacent Pony example).
69
+ Cards show short codes such as `code127`; the detail view identifies the
70
+ Tareme/Tsurime type and the prompt tag used for that sample. Thumbnails, details,
71
+ and zoom use the same code-specific image. Catalog sample references can be
72
+ refreshed without changing registered weights or the pinned revision with
73
+ `python scripts/update_jujo_catalog.py --eyecolle-samples-only`.
74
+ Comic eyes (registered Types 1/4) and Half closed eyes helper v3 (Types 1/2)
75
+ also use the matching type/version sample, not a shared header or thumbnail.
76
+ Pupil collection Types 1-6 use CSS framing of the corresponding labeled cell
77
+ in the author's unmodified comparison sheet in cards, detail, and zoom. A link
78
+ opens the complete Original/Types 1-6 sheet; no cropped image is redistributed,
79
+ and these samples are not labelled as Illustrious output without author evidence.
80
+ Refresh only these 10 sample entries (preserving weights and revisions) with
81
+ `python scripts/update_jujo_catalog.py --eye-types-samples-only`.
82
+
83
+ Viewing samples or choosing a destination does not change generation settings.
84
+ In the details, select LoRA 1-4 and a strength, then explicitly apply to replace
85
+ that slot's file/strength and enable it. Other slots retain their settings.
86
+ The initial destination prioritizes an empty slot, then a disabled slot; if
87
+ all four are active, the user must choose a destination. The current contents
88
+ of that slot are displayed before applying. Standard recommended presets
89
+ still control LoRA 4 when a preset is selected afterward.
90
+
91
+ The browser only exposes private WebUI events; the public generation/MCP
92
+ contract is unchanged. CPU-only regression checks are available with
93
+ `python -m unittest discover -s tests -p 'test_jujo*.py'` and the local
94
+ `tests/lora_ui_demo.py` + Playwright `tests/jujo_browser_ui.cjs` fixture.
95
+
96
+ Additional JujoHotaru weights download on first enabled use, inside the serialized
97
+ job but before the ZeroGPU worker is requested, then reuse the local cache.
98
+
99
+ ## Remember previous WebUI settings
100
+
101
+ ### Named favorites
102
+
103
+ Name search filters the current mode's list by substring (case/width-insensitive).
104
+ An empty query shows all entries. A selection hidden by search is cleared to
105
+ prevent accidental overwrite/deletion. Search never changes stored entries;
106
+ Export and Clear All always cover the entire current mode, even while filtered.
107
+
108
+ Export filenames follow the displayed project name:
109
+ `anima-collection-plus-mcp-favorites-anima.json` and
110
+ `anima-collection-plus-mcp-favorites-illustrious.json`.
111
+ New JSON uses the `anima-collection-plus-mcp-favorites` format identifier;
112
+ legacy `imggen-favorites` JSON is still importable. Existing browser storage
113
+ keys are deliberately unchanged, so saved settings/favorites need no migration.
114
+
115
+ The Japanese UI labels Apply as **設定を読み込む** and explains that it fills
116
+ the controls without generating an image. Settings/favorites panels disclose
117
+ that clearing cookies/site data or resetting the browser removes local saves
118
+ (ordinary image/file cache clearing normally does not), private sessions are
119
+ temporary, and JSON export is needed for favorites backup/migration.
120
+
121
+ Anima and Illustrious each provide an initially collapsed **お気に入り** panel.
122
+ Save up to 100 named configurations per mode in this browser, independently of
123
+ automatic settings storage. Explicit Add/Overwrite includes both prompts,
124
+ checkpoint, sampling, dimensions, all LoRA slots and ILL VAE/embeddings.
125
+ Apply restores manual values without running generation or reapplying preset defaults.
126
+ Choose the input seed (`-1` stays random) or the latest completed generation's
127
+ actual seed. Favorite seed values are exported as decimal strings and retained
128
+ server-side per session on Apply to avoid browser rounding of 64-bit seeds;
129
+ manual Seed edits clear that exact-seed override.
130
+
131
+ Add, Apply, Overwrite, Delete, Export, Import and Clear All are available.
132
+ Overwrite/Delete/Clear require confirmation. Clear affects only the current
133
+ mode's favorites, not current inputs or automatic settings. JSON export covers
134
+ all favorites in the current mode. Import appends without overwriting same-name
135
+ entries, validates format/version/family, field types/ranges and limits (100 entries,
136
+ 5 MB, 80-character names). Unknown fields are stripped; missing choices fall
137
+ back with a warning on Apply, and missing LoRAs are disabled. Import does not
138
+ fetch URLs, download models or execute code. No images or credentials are saved.
139
+ JSON includes prompts: take care when sharing. Browser data is not account-synced;
140
+ export/import is the migration/backup method. Storage failure reports an error
141
+ without replacing the current list. UI events may send settings to the Space,
142
+ but no favorites are written to server-side files. Public MCP API is unchanged.
143
+
144
+ ### Automatic settings
145
+
146
+ Each generation page also displays a daily-count notice near the bottom:
147
+ successful generations are counted per model to inform lineup replacements.
148
+ The statistics contain only date, mode, model name and aggregate count, not
149
+ prompts, generated images or identifying user information. This disclosure
150
+ describes the application counter, not Hugging Face's separate platform logs.
151
+
152
+ Each mode has a separate, initially collapsed **設定の保存・初期化(このブラウザ)**
153
+ panel. Settings storage and prompt storage are both ON by default. Existing
154
+ saved ON/OFF preferences are respected. Checkpoint,
155
+ preset selection, dimensions, seed input (including `-1` random), sampling,
156
+ LoRA file/strength/enabled state, and Illustrious VAE/embeddings are restored
157
+ automatically, without opening the panel or starting generation. Restored manual
158
+ adjustments take precedence over preset defaults. Presets still apply normally
159
+ when the user explicitly selects one.
160
+
161
+ The implementation uses a small Gradio HTML/localStorage bridge with stable,
162
+ versioned, per-family keys, so server restarts do not invalidate saved settings.
163
+ It does not depend on a browser encryption key or claim to protect secrets.
164
+ Data is local to that browser/profile and is not synced across devices or HF
165
+ accounts. Shared-browser users can see each other's saved preferences.
166
+
167
+ Prompt storage can be disabled independently. Disabling it removes both saved prompts;
168
+ disabling settings storage or choosing **保存設定を削除** removes settings and
169
+ leaves only an OFF preference marker. **初期設定に戻す** resets the current mode's
170
+ values; it respects the current storage preferences. No credentials or images
171
+ are saved. Restored data is allowlisted and range-checked against the current
172
+ mode's controls; missing/removed LoRAs are disabled. Prompts remain plain text.
173
+ Unavailable/corrupt localStorage falls back to normal initial UI settings.
174
+
175
+ Persistence is not a privacy boundary: UI events and restoration can send field
176
+ values to the Space. This feature does not write prompts to server-side files.
177
+ Storage/restore callbacks are private WebUI events and leave `generate_image`
178
+ unchanged. CPU browser regression: `node tests/browser_settings_ui.cjs` against
179
+ `tests/lora_ui_demo.py` (PC and mobile; no model/GPU required).
180
+
181
+ ## Daily generation counts (best effort)
182
+
183
+ Successful WebUI and MCP generations share one in-memory counter outside the
184
+ ZeroGPU worker. One returned image increments the actual checkpoint's count by
185
+ one, using its completion date in **Asia/Tokyo**. Failed calls do not increment.
186
+ Only dates, model families, checkpoint names, and counts are stored — no image,
187
+ prompt, IP address, user identity, or job history.
188
+
189
+ A single daemon thread restores the saved JSON on startup and writes a snapshot
190
+ every **300 seconds**, only when counts have changed. Generation never waits
191
+ for storage I/O. Save failures are logged and retried next interval. Abrupt
192
+ shutdown can lose unsaved counts (normally about the last five minutes; longer
193
+ during storage failure). This is operational telemetry, not billing-grade
194
+ accounting. It assumes **one Space process / replica** writes each statistics
195
+ directory; do not share it between replicas or separate Spaces.
196
+
197
+ ### Public statistics viewer
198
+
199
+ The bottom **利用状況・生成統計** accordion is collapsed by default and publicly
200
+ readable without login. `ADMIN_USERS` is no longer used in this application.
201
+ Only model-level aggregate counts are displayed, not prompts, images or user
202
+ information. The counts help inform model replacements. Report events remain
203
+ private in the Gradio API listing and outside the generation queue; this is not
204
+ an access restriction. Public `generate_image` remains unchanged.
205
+
206
+ The viewer reads `GENERATION_STATS_DIR/daily-counts.json` from the existing
207
+ read-write bucket mount without flushing or changing that file. Yesterday and
208
+ today use JST; the second table shows per-model monthly totals, newest month
209
+ first, including totals and currently registered models with zero counts.
210
+ Historically recorded models remain visible even after removal from selectors.
211
+ The view includes fetch/file-update times and a reminder of the normal five-minute
212
+ save lag and partial current month. Missing, unset or corrupt storage shows an
213
+ error instead of misleading zero counts; no filesystem details are exposed.
214
+ Statistics remain private in `yamiuchi/anima-collection-stats`; do not expose the
215
+ mount through Gradio static/allowed paths. No extra token scope or bucket
216
+ visibility change is needed for mounted reads. Only the aggregate report is public;
217
+ the bucket itself need not be made public. Forks must configure their own mount
218
+ and must not share the same writable statistics directory with another Space.
219
+
220
+ ### Persistent storage setup
221
+
222
+ 1. Create a **private HF Storage Bucket** for statistics and attach it to the
223
+ Space as a read-write volume, for example at `/data/stats`. Preserve existing
224
+ mounted volumes when adding it. Bucket creation/billing and the volume must
225
+ be configured separately; this code does not provision or change resources.
226
+ 2. Set the Space variable `GENERATION_STATS_DIR=/data/stats` (or the actual
227
+ writable mount path). The Space must have the required bucket write access.
228
+ 3. The resulting file is `/data/stats/daily-counts.json`. It restores automatically
229
+ on subsequent starts. Keep it outside Gradio static/allowed file paths.
230
+
231
+ With the variable unset, counts are **memory-only**, and startup logs a warning;
232
+ the app does not pretend an ephemeral local directory is persistent. For local
233
+ development, point the variable at the ignored `generation-stats/` directory.
234
+ The mounted model-repository volume is read-only and is not a statistics store.
235
+ See [HF Spaces storage](https://huggingface.co/docs/hub/spaces-storage) for
236
+ Bucket volumes and storage pricing. No bucket or Space settings were created
237
+ automatically as part of the implementation.
238
+
239
+ Example JSON shape:
240
+
241
+ ```json
242
+ {"version":1,"timezone":"Asia/Tokyo","counts":{"2026-10-14":{"anima":{"anima-turbo-v1.1.safetensors":12},"illustrious":{"waiIllustriousSDXL_v170.safetensors":7}}}}
243
+ ```
244
+
245
+ CPU regression tests: `python -m unittest discover -s tests -p 'test_*.py'`.
246
+ They are not downloaded or preloaded onto the GPU at startup. Download time is
247
+ additional wall-clock waiting time, outside the decorated GPU generation call.
248
+ Using every optional weight over time can add about 12.53 GB of disk usage.
249
+
250
+ The pinned metadata and author licenses/notices are in `assets/jujo/`.
251
+ Anima derivatives use the CircleStone Labs Non-Commercial License; the SDXL
252
+ collection is offered under MIT or CreativeML Open RAIL-M. The WebUI credits
253
+ link to the author's original distribution pages and the applicable terms.
254
+ `python scripts/update_jujo_catalog.py` refreshes metadata without downloading
255
+ model weights; review compatibility and sample references before deploying it.
256
+
257
+ ## Tag completion
258
+
259
+ Anima and Illustrious provide tag completion in both Prompt and Negative prompt.
260
+ Type at least two characters to see up to 20 suggestions. Use Up/Down to select,
261
+ Tab/Enter or a click to insert, Esc to dismiss, and Shift+Enter for a newline.
262
+ The controls below each prompt can disable completion or filter tag categories.
263
+ Suggestions include tag aliases and use the bundled Danbooru CSV plus quality
264
+ tags. The current tag is replaced while prompt weights and other text are kept.
265
+ CSV data loads once per browser page; searches run in the browser without GPU
266
+ time or generation queue use. Public MCP inputs and outputs remain unchanged.
267
+
268
+ See [tag data, attribution and UI checks](tags/README.md).
269
+
270
+ ## Models
271
+
272
+ | File | Source |
273
+ | --- | --- |
274
+ | 7 Anima checkpoints (2B + 2.9B bf16 UNets) | `circlestone-labs/Anima`, `Gazingstars123/Anima-2.9B`, `yamiuchi/civitai-collections` |
275
+ | 6 Anima LoRAs | `yamiuchi/civitai-collections` `lora/anima/` |
276
+ | `qwen_3_06b_base.safetensors` (text encoder) | `circlestone-labs/Anima` |
277
+ | `qwen_image_vae.safetensors` (VAE) | `circlestone-labs/Anima` |
278
+ | 4 Illustrious checkpoints (`waiIllustriousSDXL_v170`, `kiwimixXL_v3`, `mhillustrious_v10`, `zukiAnimeILL_v50`) | `yamiuchi/civitai-collections` `checkpoints/sdxl/` |
279
+ | `Detail_Tweaker_Illustrious_BSY_V3.safetensors` (Illustrious detail LoRA) | `Stableyogi/Detail-Tweaker-Illustrious` |
280
+ | `Hyper-SDXL-8steps-CFG-lora.safetensors` | `ByteDance/Hyper-SD` |
281
+ | `dmd2_sdxl_4step_lora.safetensors` | `yamiuchi/civitai-collections` `lora/sdxl/` |
282
+ | `sdxl_vae.safetensors` | `stabilityai/sdxl-vae` |
283
+ | `illustriousXLV10_v10.safetensors` | `yamiuchi/civitai-collections` `vae/sdxl/` |
284
+
285
+ The Illustrious WebUI exposes SDXL LoRAs, including explicitly labelled, unverified Pony releases. It never
286
+ offers the separate Anima LoRA list.
287
+
288
+ ## Persistent model volume (`imggen-mcp`)
289
+
290
+ The `imggen-mcp` clone mounts the private `yamiuchi/civitai-collections`
291
+ model repository read-only at `/mnt/models`. At startup, files found in that
292
+ volume are symlinked into the ComfyUI model directories, so the large private
293
+ checkpoints and LoRAs are not copied into the ephemeral Space container. If
294
+ the volume is unavailable, the app retains the existing authenticated Hub
295
+ download fallback. The public Space/API contract is unchanged.
296
+
297
+ ## MCP / Gradio API compatibility
298
+
299
+ The public endpoint remains `generate_image`. Its input types, labels, order,
300
+ and two outputs are unchanged:
301
+
302
+ 1. checkpoint, prompt, negative prompt
303
+ 2. width, height, seed, steps, CFG, sampler, scheduler
304
+ 3. four repetitions of LoRA enable, filename, and strength
305
+
306
+ Existing Anima clients continue to work. To call Illustrious through the same
307
+ endpoint, select `Illustrious-XL-v2.0.safetensors` and disable all Anima LoRA
308
+ slots. The public endpoint keeps its existing argument list and uses the
309
+ checkpoint-embedded VAE for Illustrious; VAE selection is WebUI-only.
310
+
311
+ ## Setup
312
+
313
+ 1. Create a Space with Gradio SDK, hardware **ZeroGPU**, size `large`.
314
+ 2. First cold start clones the pinned ComfyUI commit and downloads the declared
315
+ Anima and Illustrious assets; this is a large download.
316
+ 3. Optional: `HF_TOKEN` secret if any repo becomes gated (none are today).
317
+ 4. Optional: `GPU_DURATION` env var (default 30s) to tune the ZeroGPU window.
318
+
319
+ Workflow: [`image_anima_base_v1.json`](https://github.com/Comfy-Org/workflow_templates/blob/main/templates/image_anima_base_v1.json).
320
+
321
+ ## Prompting (from the official Anima README)
322
+
323
+ - Defaults: 30 steps, CFG 4, `er_sde` / `simple`.
324
+ - Recommended positive prefix: `masterpiece, best quality, score_7, safe, `
325
+ - Recommended negative: `worst quality, low quality, score_1, score_2, score_3, artist name, blurry, jpeg artifacts, chromatic aberration`
326
+ - Use lowercase Danbooru-style tags, spaces instead of underscores; score tags keep underscores; prefix artist tags with `@`.
327
+
328
+ ## Illustrious defaults
329
+
330
+ - Resolution: 1:1 (1024x1024)
331
+ - WAI standard: 30 steps, CFG 5, `euler_ancestral` / `normal`, with DMD2 disabled
332
+ - DMD2 fast preset: 8 steps, CFG 1.6, `lcm` / `normal`, LoRA 4 strength 0.7
333
+ - CLIP skip: 2 (`stop_at_clip_layer=-2`)
334
+ - VAE: checkpoint-embedded by default; Stability AI SDXL VAE and Illustrious XL v1.0 VAE are selectable in the WebUI
335
+ - Positive prefix: `masterpiece, best quality, amazing quality,`
336
+ - Negative default: `bad quality,worst quality,worst detail,sketch,censor, bad hands, extra arms, extra hands, extra fingers, long body, logo, emblem, text,`
337
+ - Negative Embeddings are selectable but start disabled. Use only embeddings compatible with the checkpoint family.
338
+ - Excessive CFG or strong weighting of Negative tags can destabilize saturation, outlines, and composition.
admin_stats.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public, read-only aggregate reports from the mounted statistics bucket.
2
+
3
+ The module name is retained for compatibility with existing UI fixtures.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+
11
+ import gradio as gr
12
+
13
+ from generation_stats import JST
14
+
15
+ LOGGER = logging.getLogger(__name__)
16
+ MAX_BYTES = 10_000_000
17
+
18
+
19
+ def make_report(raw, registered, now=None):
20
+ now = (now or datetime.now(JST)).astimezone(JST)
21
+ today = now.date()
22
+ yesterday = today - timedelta(days=1)
23
+ if (not isinstance(raw, dict) or raw.get("version") != 1
24
+ or raw.get("timezone") != "Asia/Tokyo" or not isinstance(raw.get("counts"), dict)):
25
+ raise ValueError("invalid statistics schema")
26
+ counts = raw["counts"]
27
+ known = {(family, name) for family, names in registered.items() for name in names}
28
+ months = {today.strftime("%Y-%m")}
29
+ monthly = {}
30
+ for day, families in counts.items():
31
+ parsed = datetime.strptime(day, "%Y-%m-%d").date()
32
+ if parsed.isoformat() != day or parsed > today or not isinstance(families, dict):
33
+ raise ValueError("invalid statistics day")
34
+ month = day[:7]
35
+ months.add(month)
36
+ for family, models in families.items():
37
+ if family not in ("anima", "illustrious") or not isinstance(models, dict):
38
+ raise ValueError("invalid statistics family")
39
+ for name, count in models.items():
40
+ if (not isinstance(name, str) or not name or len(name) > 256
41
+ or type(count) is not int or not 0 <= count <= 10**12):
42
+ raise ValueError("invalid statistics count")
43
+ known.add((family, name))
44
+ key = (month, family, name)
45
+ monthly[key] = monthly.get(key, 0) + count
46
+ labels = {"anima": "Anima", "illustrious": "Illustrious"}
47
+ models = sorted(known)
48
+ daily_rows = [[labels[family], name,
49
+ counts.get(yesterday.isoformat(), {}).get(family, {}).get(name, 0),
50
+ counts.get(today.isoformat(), {}).get(family, {}).get(name, 0)]
51
+ for family, name in models]
52
+ daily_rows.append(["合計", "", sum(row[2] for row in daily_rows), sum(row[3] for row in daily_rows)])
53
+ month_rows = []
54
+ for month in sorted(months, reverse=True):
55
+ rows = [[month, labels[family], name, monthly.get((month, family, name), 0)] for family, name in models]
56
+ month_rows.extend(rows)
57
+ month_rows.append([month, "合計", "", sum(row[3] for row in rows)])
58
+ return {
59
+ "daily_headers": ["モード", "モデル", f"昨日 {yesterday.isoformat()}", f"今日 {today.isoformat()}"],
60
+ "daily": daily_rows, "monthly": month_rows,
61
+ "fetched_at": now.strftime("%Y-%m-%d %H:%M:%S JST"),
62
+ }
63
+
64
+
65
+ class AdminStatistics:
66
+ def __init__(self, path, registered):
67
+ self.path = Path(path) if path is not None else None
68
+ self.registered = registered
69
+
70
+ def fetch(self):
71
+ # Public aggregates only. No flush, GPU call or Hub upload.
72
+ if self.path is None:
73
+ raise ValueError("GENERATION_STATS_DIR is not configured")
74
+ stat = self.path.stat()
75
+ if stat.st_size > MAX_BYTES:
76
+ raise ValueError("statistics file too large")
77
+ contents = self.path.read_bytes()
78
+ if len(contents) > MAX_BYTES:
79
+ raise ValueError("statistics file too large")
80
+ report = make_report(json.loads(contents), self.registered)
81
+ report["saved_at"] = datetime.fromtimestamp(stat.st_mtime, JST).strftime("%Y-%m-%d %H:%M:%S JST")
82
+ return report
83
+
84
+
85
+ def build_admin_statistics(blocks, counter, registered):
86
+ service = AdminStatistics(counter.path, registered)
87
+ with gr.Accordion("利用状況・生成統計", open=False, elem_classes=["admin-statistics"]):
88
+ gr.Markdown("モデル入れ替えの参考として、モデル別の生成件数を公開しています。ログイン不要で閲覧できます。プロンプト・画像・利用者情報は含みません。")
89
+ with gr.Group() as panel:
90
+ status = gr.Markdown()
91
+ gr.Markdown("### 昨日・今日のモデル別生成件数")
92
+ daily = gr.Dataframe(headers=["モード", "モデル", "昨日", "今日"],
93
+ datatype=["str", "str", "number", "number"], value=[],
94
+ interactive=False, type="array", wrap=True)
95
+ gr.Markdown("### 月別合計(新しい月から順に表示)")
96
+ monthly = gr.Dataframe(headers=["年月", "モード", "モデル", "生成枚数"],
97
+ datatype=["str", "str", "str", "number"], value=[],
98
+ interactive=False, type="array", wrap=True)
99
+ refresh = gr.Button("生成統計を更新")
100
+
101
+ def refresh_statistics():
102
+ try:
103
+ report = service.fetch()
104
+ except Exception:
105
+ LOGGER.warning("Public statistics read failed", exc_info=True)
106
+ # Do not render exception details, filesystem paths or stale counts.
107
+ return gr.update(visible=True), "保存済み統計を読み込めません。バケットの接続・保存状態を確認して更新してください。", gr.update(value=[]), gr.update(value=[])
108
+ text = (f"取得日時:{report['fetched_at']} \n統計ファイル更新日時:{report['saved_at']}\n\n"
109
+ "日本時間・全利用者のWebUI/MCP経由の成功した生成を集計しています。"
110
+ "接続済みの統計バケットを読み取っています。通常約5分間隔で保存されるため、未保存分は含みません。"
111
+ "保存障害時は遅れが大きくなる場合があります。月別合計は記録開始以降の件数で、今月は途中経過です。"
112
+ "記録のない日・モデルは0として表示します。")
113
+ return gr.update(visible=True), text, gr.update(headers=report["daily_headers"], value=report["daily"]), gr.update(value=report["monthly"])
114
+
115
+ outputs = [panel, status, daily, monthly]
116
+ refresh.click(refresh_statistics, inputs=None, outputs=outputs,
117
+ api_visibility="private", queue=False, show_progress="hidden")
118
+ blocks.load(refresh_statistics, inputs=None, outputs=outputs,
119
+ api_visibility="private", queue=False, show_progress="hidden")
app.py ADDED
@@ -0,0 +1,1677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model-family separated T2I UI over the ComfyUI backend (ZeroGPU Space).
2
+
3
+ Workflow: Comfy-Org/workflow_templates image_anima_base_v1.json
4
+ Models: Anima collection + Illustrious XL v2.0.
5
+
6
+ The public ``generate_image`` endpoint intentionally keeps the original input
7
+ order so existing Gradio API and MCP clients continue to work.
8
+ """
9
+
10
+ import os
11
+ import random
12
+ import re
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import threading
17
+ from pathlib import Path
18
+
19
+ from jujo_loras import (
20
+ CATALOGS as JUJO_CATALOGS,
21
+ JUJO_ENTRIES,
22
+ NONE_LORA,
23
+ jujo_credits,
24
+ jujo_label,
25
+ lora_selected,
26
+ pony_selection_notice,
27
+ )
28
+
29
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
30
+
31
+ import spaces # noqa: E402
32
+
33
+ COMFYUI_PATH = os.environ.get("COMFYUI_PATH", os.path.join(os.getcwd(), "ComfyUI"))
34
+ MODELS_ROOT = os.path.join(COMFYUI_PATH, "models")
35
+ CACHE_DIR = os.path.join(os.getcwd(), "hf_cache")
36
+ # A Hugging Face model-repository volume can be mounted at this path. The
37
+ # Space still works without the volume: missing files fall back to the normal
38
+ # Hub download path below. Keep the mount read-only and create symlinks into
39
+ # ComfyUI so the large private collection is never copied into the container.
40
+ MODEL_VOLUME_ROOT = os.environ.get("MODEL_VOLUME_ROOT", "/mnt/models")
41
+
42
+
43
+ COMFYUI_REPO = "https://github.com/Comfy-Org/ComfyUI.git"
44
+
45
+ # Pinned to ComfyUI v0.33.1.
46
+ COMFYUI_COMMIT = "72865f4f27eaf5396f8f36370e0a2be3a9a090ee"
47
+
48
+
49
+ def ensure_comfyui() -> None:
50
+ """Install exactly the pinned ComfyUI commit."""
51
+ nodes_py = os.path.join(COMFYUI_PATH, "nodes.py")
52
+
53
+ # Reuse an existing checkout only when it matches the pinned commit.
54
+ if os.path.isfile(nodes_py):
55
+ try:
56
+ current_commit = subprocess.check_output(
57
+ ["git", "-C", COMFYUI_PATH, "rev-parse", "HEAD"],
58
+ text=True,
59
+ ).strip()
60
+ if current_commit == COMFYUI_COMMIT:
61
+ print(f"Using pinned ComfyUI commit: {current_commit}", flush=True)
62
+ return
63
+
64
+ print(
65
+ f"ComfyUI commit mismatch: {current_commit} != {COMFYUI_COMMIT}; reinstalling.",
66
+ flush=True,
67
+ )
68
+ except (subprocess.CalledProcessError, OSError):
69
+ print("Could not verify existing ComfyUI checkout; reinstalling.", flush=True)
70
+
71
+ shutil.rmtree(COMFYUI_PATH, ignore_errors=True)
72
+
73
+ elif os.path.isdir(COMFYUI_PATH):
74
+ # Remove an incomplete checkout left by a failed startup.
75
+ shutil.rmtree(COMFYUI_PATH, ignore_errors=True)
76
+
77
+ os.makedirs(COMFYUI_PATH, exist_ok=True)
78
+
79
+ # Fetch only the pinned commit instead of following the latest master.
80
+ subprocess.run(["git", "init", COMFYUI_PATH], check=True)
81
+ subprocess.run(
82
+ ["git", "-C", COMFYUI_PATH, "remote", "add", "origin", COMFYUI_REPO],
83
+ check=True,
84
+ )
85
+ subprocess.run(
86
+ ["git", "-C", COMFYUI_PATH, "fetch", "--depth", "1", "origin", COMFYUI_COMMIT],
87
+ check=True,
88
+ )
89
+ subprocess.run(
90
+ ["git", "-C", COMFYUI_PATH, "checkout", "--detach", "FETCH_HEAD"],
91
+ check=True,
92
+ )
93
+
94
+ current_commit = subprocess.check_output(
95
+ ["git", "-C", COMFYUI_PATH, "rev-parse", "HEAD"],
96
+ text=True,
97
+ ).strip()
98
+
99
+ if current_commit != COMFYUI_COMMIT:
100
+ raise RuntimeError(
101
+ f"Unexpected ComfyUI commit: {current_commit}; expected {COMFYUI_COMMIT}"
102
+ )
103
+
104
+ print(f"Pinned ComfyUI commit loaded: {current_commit}", flush=True)
105
+
106
+
107
+ ensure_comfyui()
108
+ if COMFYUI_PATH not in sys.path:
109
+ sys.path.insert(0, COMFYUI_PATH)
110
+
111
+ import comfy.options # noqa: E402
112
+
113
+ comfy.options.enable_args_parsing()
114
+
115
+ import numpy as np # noqa: E402
116
+ import torch # noqa: E402
117
+
118
+ torch.set_grad_enabled(False)
119
+
120
+ from huggingface_hub import hf_hub_download # noqa: E402
121
+
122
+ from comfy import model_management # noqa: E402
123
+ from comfy import sample as comfy_sample # noqa: E402
124
+ from nodes import ( # noqa: E402
125
+ CheckpointLoaderSimple,
126
+ CLIPLoader,
127
+ CLIPSetLastLayer,
128
+ CLIPTextEncode,
129
+ EmptyLatentImage,
130
+ LoraLoaderModelOnly,
131
+ UNETLoader,
132
+ VAEDecode,
133
+ VAELoader,
134
+ )
135
+
136
+ # --------------------------------------------------------------------------
137
+ # Models
138
+ # --------------------------------------------------------------------------
139
+ COLLECTION_REPO = "yamiuchi/civitai-collections"
140
+ COMPANION_REPO = "circlestone-labs/Anima"
141
+ ILLUSTRIOUS_REPO = "OnomaAIResearch/Illustrious-XL-v2.0"
142
+ HYPER_SD_REPO = "ByteDance/Hyper-SD"
143
+ # Illustrious checkpoints are SDXL-based. Their embedded VAE is the default,
144
+ # with compatible external VAEs available as optional WebUI alternatives.
145
+ # Anima keeps using its Qwen VAE below.
146
+ SDXL_VAE_REPO = "stabilityai/sdxl-vae"
147
+
148
+ MODEL_CHOICES = [
149
+ ("anima-turbo-v1.1.safetensors", COMPANION_REPO, "split_files/diffusion_models", 4.18),
150
+ # ("anima-base-v1.0.safetensors", COMPANION_REPO, "split_files/diffusion_models", 4.18),
151
+ ("anima-aesthetic-v1.1.safetensors", COMPANION_REPO, "split_files/diffusion_models", 4.18),
152
+ # ("Anima-2.9B-preview-v1.safetensors", "Gazingstars123/Anima-2.9B", "", 5.84),
153
+ # ("anima_pencil-v2.1.0.safetensors", COLLECTION_REPO, "checkpoints/anima", 4.18),
154
+ ("fnMixAnimaTurbo_baseNoTurbo.safetensors", COLLECTION_REPO, "checkpoints/anima", 4.18),
155
+ ("tekitoMix_v32.safetensors", COLLECTION_REPO, "checkpoints/anima", 2.25),
156
+ # ("anima-kirazuri-v4.safetensors", "motimalu/kirazuri-anima", "split_files/diffusion_models", 4.18),
157
+ ]
158
+ MODEL_FILES = [fname for fname, _, _, _ in MODEL_CHOICES]
159
+
160
+ ILLUSTRIOUS_MODEL_CHOICES = [
161
+ ("waiIllustriousSDXL_v170.safetensors", COLLECTION_REPO, "checkpoints/sdxl", 6.94),
162
+ ("kiwimixXL_v3.safetensors", COLLECTION_REPO, "checkpoints/sdxl", 6.94),
163
+ ("mhillustrious_v10.safetensors", COLLECTION_REPO, "checkpoints/sdxl", 6.94),
164
+ ("zukiAnimeILL_v50.safetensors", COLLECTION_REPO, "checkpoints/sdxl", 7.11),
165
+ # ("Illustrious-XL-v2.0.safetensors", ILLUSTRIOUS_REPO, "", 6.94),
166
+ ]
167
+ ILLUSTRIOUS_MODEL_FILES = [fname for fname, _, _, _ in ILLUSTRIOUS_MODEL_CHOICES]
168
+ ALL_MODEL_FILES = MODEL_FILES + ILLUSTRIOUS_MODEL_FILES
169
+
170
+ LORA_FILES = [
171
+ ("Anima-P3-Aesthetics-Details_v1.0.safetensors", "lora/anima"),
172
+ ("anima-base-1-masterpiece-v51.safetensors", "lora/anima"),
173
+ ("anima-highres-aesthetic-boost.safetensors", "lora/anima"),
174
+ ("gpt-image-2_anima-base1_v1.safetensors", "lora/anima"),
175
+ ("anima-turbo-lora-v0.2.safetensors", "lora/anima"),
176
+ ("Turbo-ANIMA-v2.safetensors", "lora/anima"),
177
+ ]
178
+
179
+ # Keep family lists separate even though ComfyUI stores all LoRAs in one
180
+ # directory. This prevents accidentally applying an Anima LoRA to an SDXL /
181
+ # Illustrious checkpoint from the WebUI.
182
+ ILLUSTRIOUS_LORA_FILES: list[tuple[str, str, str]] = [
183
+ ("Detail_Tweaker_Illustrious_BSY_V3.safetensors", "Stableyogi/Detail-Tweaker-Illustrious", "",),
184
+ ("dmd2_sdxl_4step_lora.safetensors", COLLECTION_REPO, "lora/sdxl"),
185
+ ("Hyper-SDXL-8steps-CFG-lora.safetensors", HYPER_SD_REPO, ""),
186
+ ]
187
+
188
+ # Embeddings are kept separate from LoRAs and are presented only on the
189
+ # Illustrious page. The repository folders document the checkpoint family;
190
+ # ComfyUI itself loads the files from one flat ``models/embeddings`` folder.
191
+ ILLUSTRIOUS_EMBEDDING_FILES: list[tuple[str, str, str]] = [
192
+ ("negativeXL_D.safetensors", "embeddings/sdxl", "SDXL / Illustrious"),
193
+ ("negativeXL.safetensors", "embeddings/sdxl", "SDXL"),
194
+ ("EasyNegativeV2.safetensors", "embeddings/sd1.5", "SD1.5"),
195
+ ("bad-hands-5.pt", "embeddings/sd1.5", "SD1.5"),
196
+ ]
197
+
198
+ TEXT_ENCODER = ("qwen_3_06b_base.safetensors", "split_files/text_encoders")
199
+ VAE_FILE = ("qwen_image_vae.safetensors", "split_files/vae")
200
+ ILLUSTRIOUS_VAE_FILES = [
201
+ ("sdxl_vae.safetensors", SDXL_VAE_REPO, ""),
202
+ ("illustriousXLV10_v10.safetensors", COLLECTION_REPO, "vae/sdxl"),
203
+ ]
204
+ CHECKPOINT_VAE_OPTION = "__checkpoint_embedded__"
205
+ ILLUSTRIOUS_VAE_CHOICES = [
206
+ ("Checkpoint内蔵VAE(推奨)", CHECKPOINT_VAE_OPTION),
207
+ ("SDXL VAE(Stability AI)", "sdxl_vae.safetensors"),
208
+ ("Illustrious XL v1.0 VAE(バランス)", "illustriousXLV10_v10.safetensors"),
209
+ ]
210
+
211
+
212
+ # --------------------------------------------------------------------------
213
+ # Prompt macro / replacement rules
214
+ # --------------------------------------------------------------------------
215
+ # 1) <macro:KEY>
216
+ # Expands KEY using the PROMPT_MACROS mapping below.
217
+ # Unknown keys are left unchanged as-is.
218
+ #
219
+ # 2) <replace:TEXT>
220
+ # Replaces the full tag with the inner TEXT.
221
+ # Example: "<replace:blue eyes>" -> "blue eyes"
222
+ #
223
+ # 3) POSITIVE_PROMPT_REPLACE_RULES / NEGATIVE_PROMPT_REPLACE_RULES
224
+ # Additional configurable word/phrase replacements applied after the
225
+ # inline macro/replace expansion step.
226
+ #
227
+ # Example:
228
+ # PROMPT_MACROS = {
229
+ # "QUALITY": "masterpiece, best quality, score_9, score_8, score_7",
230
+ # "OUTDOOR": "outdoors, blue sky, sunlight",
231
+ # }
232
+ #
233
+ # POSITIVE_PROMPT_REPLACE_RULES = [
234
+ # {"from": "high quality", "to": "best quality", "ignore_case": True, "word_boundary": False},
235
+ # ]
236
+ #
237
+ # NEGATIVE_PROMPT_REPLACE_RULES = [
238
+ # {"from": "lowres", "to": "worst quality", "ignore_case": True, "word_boundary": True},
239
+ # ]
240
+ PROMPT_MACROS = {
241
+ # "QUALITY": "masterpiece, best quality, score_9, score_8, score_7",
242
+ # "OUTDOOR": "outdoors, blue sky, sunlight",
243
+ }
244
+
245
+ POSITIVE_PROMPT_REPLACE_RULES = [
246
+ ]
247
+
248
+ NEGATIVE_PROMPT_REPLACE_RULES = [
249
+ ]
250
+
251
+ MACRO_TAG_PATTERN = re.compile(r"<macro:([^<>]+)>")
252
+ INLINE_REPLACE_TAG_PATTERN = re.compile(r"<replace:([^<>]+)>")
253
+
254
+ def apply_macro_tags(text: str, macro_map: dict[str, str]) -> str:
255
+ """Expand <macro:KEY> tags using a predefined mapping."""
256
+ if not text:
257
+ return text
258
+
259
+ def repl(match: re.Match) -> str:
260
+ key = match.group(1).strip()
261
+ return str(macro_map.get(key, match.group(0)))
262
+
263
+ return MACRO_TAG_PATTERN.sub(repl, text)
264
+
265
+
266
+ def apply_inline_replace_tags(text: str) -> str:
267
+ """Convert <replace:TEXT> tags into TEXT."""
268
+ if not text:
269
+ return text
270
+
271
+ def repl(match: re.Match) -> str:
272
+ return match.group(1).strip()
273
+
274
+ return INLINE_REPLACE_TAG_PATTERN.sub(repl, text)
275
+
276
+
277
+ def apply_replacement_rules(text: str, rules: list[dict]) -> str:
278
+ """Apply configured replacement rules to a prompt string."""
279
+ if not text:
280
+ return text
281
+
282
+ # Longer source strings are applied first to reduce accidental partial overlaps.
283
+ sorted_rules = sorted(rules, key=lambda x: len(str(x.get("from", ""))), reverse=True)
284
+
285
+ for rule in sorted_rules:
286
+ src = str(rule.get("from", ""))
287
+ dst = str(rule.get("to", ""))
288
+ if not src:
289
+ continue
290
+
291
+ pattern = re.escape(src)
292
+ if rule.get("word_boundary", False):
293
+ pattern = r"\b" + pattern + r"\b"
294
+
295
+ flags = re.IGNORECASE if rule.get("ignore_case", True) else 0
296
+ text = re.sub(pattern, dst, text, flags=flags)
297
+
298
+ return text
299
+
300
+
301
+ def normalize_prompt_text(text: str, rules: list[dict]) -> str:
302
+ """Apply inline macro expansion and configured replacements."""
303
+ if not text:
304
+ return text
305
+ text = apply_macro_tags(text, PROMPT_MACROS)
306
+ text = apply_inline_replace_tags(text)
307
+ text = apply_replacement_rules(text, rules)
308
+ return text
309
+
310
+
311
+ def ensure_model_file(
312
+ repo_id: str,
313
+ subfolder: str,
314
+ filename: str,
315
+ dest_dir: str,
316
+ *,
317
+ min_size: int = 1_000_000,
318
+ revision: str | None = None,
319
+ ) -> str:
320
+ """Download a model file into a ComfyUI models/ subdir (idempotent)."""
321
+ dest = os.path.join(dest_dir, filename)
322
+ if os.path.isfile(dest) and os.path.getsize(dest) > min_size:
323
+ return dest
324
+
325
+ # Prefer the persistent, read-only model volume for the private collection.
326
+ # The repository layout is mirrored below /mnt/models (for example,
327
+ # /mnt/models/checkpoints/sdxl/foo.safetensors). Symlinking keeps the
328
+ # container layer and startup disk usage small while preserving ComfyUI's
329
+ # expected flat model directories.
330
+ volume_src = None
331
+ if repo_id == COLLECTION_REPO:
332
+ candidate = os.path.join(MODEL_VOLUME_ROOT, subfolder or "", filename)
333
+ if os.path.isfile(candidate):
334
+ volume_src = candidate
335
+ if volume_src:
336
+ os.makedirs(dest_dir, exist_ok=True)
337
+ if os.path.lexists(dest) and not os.path.isfile(dest):
338
+ os.unlink(dest)
339
+ if not os.path.lexists(dest):
340
+ try:
341
+ os.symlink(volume_src, dest)
342
+ except OSError as exc:
343
+ raise RuntimeError(
344
+ f"Could not link model volume file {volume_src} to {dest}."
345
+ ) from exc
346
+ print(f"Using mounted model volume: {volume_src}", flush=True)
347
+ return dest
348
+
349
+ os.makedirs(CACHE_DIR, exist_ok=True)
350
+ os.makedirs(dest_dir, exist_ok=True)
351
+ src = hf_hub_download(
352
+ repo_id=repo_id,
353
+ subfolder=subfolder or None,
354
+ filename=filename,
355
+ revision=revision,
356
+ local_dir=CACHE_DIR,
357
+ token=os.environ.get("HF_TOKEN") or None,
358
+ )
359
+ if not os.path.isfile(dest):
360
+ try:
361
+ os.replace(src, dest)
362
+ except OSError:
363
+ shutil.copy2(src, dest)
364
+ return dest
365
+
366
+
367
+ def ensure_models() -> None:
368
+ te_name, te_sub = TEXT_ENCODER
369
+ vae_name, vae_sub = VAE_FILE
370
+ ensure_model_file(COMPANION_REPO, te_sub, te_name, os.path.join(MODELS_ROOT, "text_encoders"))
371
+ ensure_model_file(COMPANION_REPO, vae_sub, vae_name, os.path.join(MODELS_ROOT, "vae"))
372
+ for ill_vae_name, ill_vae_repo, ill_vae_sub in ILLUSTRIOUS_VAE_FILES:
373
+ ensure_model_file(
374
+ ill_vae_repo,
375
+ ill_vae_sub,
376
+ ill_vae_name,
377
+ os.path.join(MODELS_ROOT, "vae"),
378
+ min_size=1_000_000,
379
+ )
380
+ for fname, repo, sub, _ in MODEL_CHOICES:
381
+ ensure_model_file(repo, sub, fname, os.path.join(MODELS_ROOT, "diffusion_models"))
382
+ for fname, sub in LORA_FILES:
383
+ ensure_model_file(COLLECTION_REPO, sub, fname, os.path.join(MODELS_ROOT, "loras"))
384
+ for fname, repo, sub, _ in ILLUSTRIOUS_MODEL_CHOICES:
385
+ ensure_model_file(repo, sub, fname, os.path.join(MODELS_ROOT, "checkpoints"))
386
+ for fname, repo, sub in ILLUSTRIOUS_LORA_FILES:
387
+ ensure_model_file(repo, sub, fname, os.path.join(MODELS_ROOT, "loras"))
388
+ for fname, sub, _family in ILLUSTRIOUS_EMBEDDING_FILES:
389
+ ensure_model_file(
390
+ COLLECTION_REPO,
391
+ sub,
392
+ fname,
393
+ os.path.join(MODELS_ROOT, "embeddings"),
394
+ min_size=0,
395
+ )
396
+
397
+
398
+ ensure_models()
399
+
400
+
401
+ def get_value_at_index(obj, index: int):
402
+ try:
403
+ return obj[index]
404
+ except KeyError:
405
+ return obj["result"][index]
406
+
407
+
408
+ def list_loras() -> list[str]:
409
+ lora_dir = os.path.join(MODELS_ROOT, "loras")
410
+ return sorted(f for f in os.listdir(lora_dir) if f.endswith(".safetensors")) if os.path.isdir(lora_dir) else []
411
+
412
+
413
+ # --------------------------------------------------------------------------
414
+ # Load models at module scope so ZeroGPU packs them at startup.
415
+ # --------------------------------------------------------------------------
416
+ unet_loader = UNETLoader()
417
+ UNETS = {
418
+ fname: unet_loader.load_unet(unet_name=fname, weight_dtype="default")
419
+ for fname, _, _, _ in MODEL_CHOICES
420
+ }
421
+
422
+ checkpoint_loader = CheckpointLoaderSimple()
423
+ ILLUSTRIOUS_CHECKPOINTS = {
424
+ fname: checkpoint_loader.load_checkpoint(ckpt_name=fname)
425
+ for fname, _, _, _ in ILLUSTRIOUS_MODEL_CHOICES
426
+ }
427
+
428
+ clip_loader = CLIPLoader()
429
+ CLIP = clip_loader.load_clip(clip_name=TEXT_ENCODER[0], type="stable_diffusion")
430
+
431
+ vae_loader = VAELoader()
432
+ VAE = vae_loader.load_vae(vae_name=VAE_FILE[0])
433
+ ILLUSTRIOUS_VAES = {
434
+ vae_name: vae_loader.load_vae(vae_name=vae_name)
435
+ for vae_name, _repo, _subfolder in ILLUSTRIOUS_VAE_FILES
436
+ }
437
+
438
+ lora_loader = LoraLoaderModelOnly()
439
+ text_encode = CLIPTextEncode()
440
+ clip_set_last_layer = CLIPSetLastLayer()
441
+ empty_latent = EmptyLatentImage()
442
+ vae_decode = VAEDecode()
443
+
444
+ model_management.load_models_gpu(
445
+ [
446
+ getattr(get_value_at_index(unet, 0), "patcher", get_value_at_index(unet, 0))
447
+ for unet in UNETS.values()
448
+ ]
449
+ + [
450
+ getattr(get_value_at_index(CLIP, 0), "patcher", get_value_at_index(CLIP, 0)),
451
+ getattr(get_value_at_index(VAE, 0), "patcher", get_value_at_index(VAE, 0)),
452
+ ]
453
+ + [
454
+ getattr(get_value_at_index(checkpoint, index), "patcher", get_value_at_index(checkpoint, index))
455
+ for checkpoint in ILLUSTRIOUS_CHECKPOINTS.values()
456
+ # Keep VAEs off this startup preload list. The selected embedded or
457
+ # external Illustrious VAE is moved as needed by the decode path.
458
+ for index in (0, 1)
459
+ ]
460
+ )
461
+
462
+
463
+ # --------------------------------------------------------------------------
464
+ # Inference
465
+ # --------------------------------------------------------------------------
466
+ import gradio as gr # noqa: E402 # needed for gr.Progress default arg below
467
+ from tag_completion import prompt_with_completion # noqa: E402
468
+ from jujo_browser import build_jujo_browser # noqa: E402
469
+ from browser_settings import bind_browser_settings # noqa: E402
470
+ from generation_stats import stats as generation_stats # noqa: E402
471
+ from admin_stats import build_admin_statistics # noqa: E402
472
+
473
+ generation_lock = threading.Lock()
474
+
475
+
476
+ def model_volume_status() -> str:
477
+ if os.path.isdir(MODEL_VOLUME_ROOT):
478
+ return f"mounted at `{MODEL_VOLUME_ROOT}` (volume-first for {COLLECTION_REPO})"
479
+ return "not mounted (Hub download fallback)"
480
+
481
+
482
+ STATIC_INFO = (
483
+ f"**Loaded models:** {len(MODEL_CHOICES)}x Anima + "
484
+ f"{len(ILLUSTRIOUS_MODEL_CHOICES)}x Illustrious checkpoints "
485
+ f"(~{sum(size for _, _, _, size in MODEL_CHOICES + ILLUSTRIOUS_MODEL_CHOICES) + 1.19 + 0.25:.0f} GB, excluding LoRAs)\n"
486
+ "**Prompt weighting:** supported, e.g. `(tag)`, `(tag:1.2)`, `(tag:0.7)`.\n"
487
+ f"**Model volume:** {model_volume_status()}"
488
+ )
489
+
490
+ MODEL_FAMILY = {
491
+ **{name: "anima" for name in MODEL_FILES},
492
+ **{name: "illustrious" for name in ILLUSTRIOUS_MODEL_FILES},
493
+ }
494
+
495
+ FAMILY_LORA_NAMES = {
496
+ "anima": {name for name, _ in LORA_FILES} | set(JUJO_ENTRIES["anima"]),
497
+ "illustrious": {name for name, _, _ in ILLUSTRIOUS_LORA_FILES} | set(JUJO_ENTRIES["illustrious"]),
498
+ }
499
+
500
+ EMBEDDING_PATHS = {
501
+ name: os.path.join(MODELS_ROOT, "embeddings", name)
502
+ for name, _subfolder, _family_label in ILLUSTRIOUS_EMBEDDING_FILES
503
+ }
504
+ FAMILY_EMBEDDING_NAMES = {
505
+ "anima": set(),
506
+ "illustrious": set(EMBEDDING_PATHS),
507
+ }
508
+
509
+
510
+ def model_components(model_name: str, vae_name: str | None = None):
511
+ """Return model, CLIP, selected VAE, family and displayed VAE name."""
512
+ family = MODEL_FAMILY.get(model_name)
513
+ if family == "anima":
514
+ return (
515
+ get_value_at_index(UNETS[model_name], 0),
516
+ get_value_at_index(CLIP, 0),
517
+ get_value_at_index(VAE, 0),
518
+ family,
519
+ VAE_FILE[0],
520
+ )
521
+ if family == "illustrious":
522
+ checkpoint = ILLUSTRIOUS_CHECKPOINTS[model_name]
523
+ clip = get_value_at_index(
524
+ clip_set_last_layer.set_last_layer(
525
+ clip=get_value_at_index(checkpoint, 1),
526
+ stop_at_clip_layer=-2,
527
+ ),
528
+ 0,
529
+ )
530
+ selected_vae_name = vae_name or CHECKPOINT_VAE_OPTION
531
+ if selected_vae_name == CHECKPOINT_VAE_OPTION:
532
+ selected_vae = get_value_at_index(checkpoint, 2)
533
+ displayed_vae_name = "Checkpoint内蔵VAE"
534
+ elif selected_vae_name in ILLUSTRIOUS_VAES:
535
+ selected_vae = get_value_at_index(ILLUSTRIOUS_VAES[selected_vae_name], 0)
536
+ displayed_vae_name = selected_vae_name
537
+ else:
538
+ raise gr.Error(
539
+ f"{selected_vae_name} is not registered as an Illustrious VAE."
540
+ )
541
+ return (
542
+ get_value_at_index(checkpoint, 0),
543
+ clip,
544
+ selected_vae,
545
+ family,
546
+ displayed_vae_name,
547
+ )
548
+ raise gr.Error(f"Unknown checkpoint: {model_name}")
549
+
550
+
551
+ # GPU stats are only real inside the ZeroGPU worker, so they are fetched at
552
+ # the end of each generation rather than at startup.
553
+ def gpu_stats() -> str:
554
+ try:
555
+ name = torch.cuda.get_device_name(0)
556
+ free, total = torch.cuda.mem_get_info(0)
557
+ used = total - free
558
+ except Exception as exc:
559
+ return f"**GPU:** unavailable ({exc})"
560
+ return (
561
+ f"**GPU:** {name}\n"
562
+ f"**VRAM:** {used / 1e9:.1f} GB used / {total / 1e9:.1f} GB total ({free / 1e9:.1f} GB free)"
563
+ )
564
+
565
+
566
+ def apply_negative_embeddings(
567
+ negative_prompt: str,
568
+ family: str,
569
+ embedding_names: list[str] | None,
570
+ ) -> tuple[str, list[str]]:
571
+ """Prefix selected ComfyUI embedding references to a negative prompt."""
572
+ if not embedding_names:
573
+ return negative_prompt, []
574
+
575
+ allowed = FAMILY_EMBEDDING_NAMES.get(family, set())
576
+ selected: list[str] = []
577
+ for name in embedding_names:
578
+ if not name:
579
+ continue
580
+ if name not in allowed:
581
+ raise gr.Error(
582
+ f"{name} is not registered as an embedding for the {family} page."
583
+ )
584
+ if not os.path.isfile(EMBEDDING_PATHS[name]):
585
+ raise gr.Error(
586
+ f"Embedding file is not available: {name}. "
587
+ "Restart the Space to download the registered assets."
588
+ )
589
+ if name not in selected:
590
+ selected.append(name)
591
+
592
+ if not selected:
593
+ return negative_prompt, []
594
+
595
+ existing = negative_prompt or ""
596
+ tokens = [f"embedding:{name}" for name in selected]
597
+ missing_tokens = [token for token in tokens if token not in existing]
598
+ if not missing_tokens:
599
+ return existing, selected
600
+
601
+ prefix = ", ".join(missing_tokens)
602
+ effective = f"{prefix}, {existing}" if existing.strip() else prefix
603
+ return effective, selected
604
+
605
+ @spaces.GPU(duration=int(os.environ.get("GPU_DURATION", "30")))
606
+ @torch.inference_mode()
607
+ def _generate_image_core(
608
+ model_name: str,
609
+ prompt: str,
610
+ negative_prompt: str,
611
+ width: int,
612
+ height: int,
613
+ seed: int,
614
+ steps: int,
615
+ cfg: float,
616
+ sampler_name: str,
617
+ scheduler: str,
618
+ enable_lora: bool,
619
+ lora_name: str,
620
+ lora_strength: float,
621
+ enable_lora_2: bool,
622
+ lora_name_2: str,
623
+ lora_strength_2: float,
624
+ enable_lora_3: bool,
625
+ lora_name_3: str,
626
+ lora_strength_3: float,
627
+ enable_lora_4: bool,
628
+ lora_name_4: str,
629
+ lora_strength_4: float,
630
+ embedding_names: list[str] | None,
631
+ vae_name: str | None,
632
+ progress: gr.Progress = gr.Progress(track_tqdm=False),
633
+ ) -> tuple[np.ndarray, str, str, str]:
634
+ """
635
+ Generate one image using an Anima or Illustrious checkpoint.
636
+
637
+ Returns:
638
+ Generated image, base generation information, effective positive prompt,
639
+ and effective negative prompt.
640
+ """
641
+ width = max(512, min(1536, int(width) // 16 * 16))
642
+ height = max(512, min(1536, int(height) // 16 * 16))
643
+ seed = int(seed) if int(seed) >= 0 else random.randint(1, 2**63)
644
+ steps = max(1, min(60, int(steps)))
645
+ cfg = float(max(1.0, min(8.0, cfg)))
646
+
647
+ lora_slots = [
648
+ (1, bool(enable_lora), lora_name, float(lora_strength)),
649
+ (2, bool(enable_lora_2), lora_name_2, float(lora_strength_2)),
650
+ (3, bool(enable_lora_3), lora_name_3, float(lora_strength_3)),
651
+ (4, bool(enable_lora_4), lora_name_4, float(lora_strength_4)),
652
+ ]
653
+
654
+ model, clip, vae, family, displayed_vae_name = model_components(model_name, vae_name)
655
+ applied_loras: list[str] = []
656
+ for slot_no, enabled, current_lora_name, current_lora_strength in lora_slots:
657
+ if not lora_selected(enabled, current_lora_name):
658
+ continue
659
+ if current_lora_name not in FAMILY_LORA_NAMES[family]:
660
+ raise gr.Error(
661
+ f"{current_lora_name} is not registered as a {family} LoRA. "
662
+ "Select a LoRA from the same model-family page."
663
+ )
664
+ current_lora_strength = max(0.0, min(2.0, current_lora_strength))
665
+ model = get_value_at_index(
666
+ lora_loader.load_lora_model_only(
667
+ model=model,
668
+ lora_name=current_lora_name,
669
+ strength_model=current_lora_strength,
670
+ ),
671
+ 0,
672
+ )
673
+ applied_loras.append(f"{slot_no}:{current_lora_name}@{current_lora_strength:g}")
674
+
675
+ effective_prompt = normalize_prompt_text(prompt, POSITIVE_PROMPT_REPLACE_RULES)
676
+ effective_negative_prompt = normalize_prompt_text(negative_prompt, NEGATIVE_PROMPT_REPLACE_RULES)
677
+ effective_negative_prompt, applied_embeddings = apply_negative_embeddings(
678
+ effective_negative_prompt,
679
+ family,
680
+ embedding_names,
681
+ )
682
+
683
+ positive = text_encode.encode(text=effective_prompt, clip=clip)
684
+ negative = text_encode.encode(text=effective_negative_prompt, clip=clip)
685
+
686
+ latent = empty_latent.generate(width=width, height=height, batch_size=1)
687
+ lat = get_value_at_index(latent, 0)
688
+ latent_image = comfy_sample.fix_empty_latent_channels(
689
+ model, lat["samples"],
690
+ lat.get("downscale_ratio_spacial", None),
691
+ lat.get("downscale_ratio_temporal", None),
692
+ )
693
+
694
+ progress(0, desc="Generating")
695
+
696
+ def step_cb(step: int, _denoised, _x, total_steps: int) -> None:
697
+ progress((step, total_steps), desc=f"Sampling {step + 1}/{total_steps}")
698
+
699
+ samples = comfy_sample.sample(
700
+ model=model,
701
+ noise=comfy_sample.prepare_noise(latent_image, seed, None),
702
+ steps=steps,
703
+ cfg=cfg,
704
+ sampler_name=sampler_name,
705
+ scheduler=scheduler,
706
+ positive=get_value_at_index(positive, 0),
707
+ negative=get_value_at_index(negative, 0),
708
+ latent_image=latent_image,
709
+ denoise=1.0,
710
+ callback=step_cb,
711
+ disable_pbar=True,
712
+ seed=seed,
713
+ )
714
+ sampled = {"samples": samples}
715
+
716
+ progress(1, desc="Decoding")
717
+ decoded = vae_decode.decode(samples=sampled, vae=vae)
718
+ image = get_value_at_index(decoded, 0)[0]
719
+ img_np = (
720
+ torch.nan_to_num(image, nan=0.0, posinf=1.0, neginf=0.0)
721
+ .mul(255)
722
+ .clamp_(0, 255)
723
+ .byte()
724
+ .cpu()
725
+ .numpy()
726
+ )
727
+
728
+ base_info = (
729
+ STATIC_INFO
730
+ + f"\n{gpu_stats()}"
731
+ + f"\n**Last run:** {family.title()} / {model_name} | {width}x{height} | {steps} steps | CFG {cfg} | seed {seed}"
732
+ + f"\n**Sampler:** {sampler_name} | **Scheduler:** {scheduler}"
733
+ + f"\n**VAE:** {displayed_vae_name}"
734
+ + f"\n**LoRA:** {', '.join(applied_loras) if applied_loras else 'disabled'}"
735
+ + f"\n**Negative Embeddings:** {', '.join(applied_embeddings) if applied_embeddings else 'disabled'}"
736
+ )
737
+ return img_np, base_info, effective_prompt, effective_negative_prompt
738
+
739
+
740
+ def serialized_generate(*args, **kwargs):
741
+ """Serialize model generation before entering the ZeroGPU decorator."""
742
+ with generation_lock:
743
+ # Both callers use the unchanged generation argument order. Prepare
744
+ # optional author LoRAs on CPU, before requesting the ZeroGPU worker.
745
+ model_name = args[0]
746
+ family = MODEL_FAMILY.get(model_name)
747
+ for slot in range(4):
748
+ enabled, name, _strength = args[10 + slot * 3:13 + slot * 3]
749
+ if not lora_selected(enabled, name):
750
+ continue
751
+ if name not in FAMILY_LORA_NAMES.get(family, set()):
752
+ raise gr.Error(f"{name} is not registered as a {family} LoRA.")
753
+ entry = JUJO_ENTRIES[family].get(name)
754
+ if entry:
755
+ catalog = JUJO_CATALOGS[family]
756
+ progress = args[-1]
757
+ progress(0, desc=f"LoRAを準備中: {name}(初回はダウンロード)")
758
+ ensure_model_file(
759
+ catalog["repo"],
760
+ entry["path"].rsplit("/", 1)[0] if "/" in entry["path"] else "",
761
+ name,
762
+ os.path.join(MODELS_ROOT, "loras"),
763
+ min_size=1_000_000,
764
+ revision=catalog["revision"],
765
+ )
766
+ result = _generate_image_core(*args, **kwargs)
767
+ # This runs in the parent process after the ZeroGPU call returns.
768
+ # Only a tiny in-memory update is performed, shared by UI and MCP.
769
+ if result is not None and result[0] is not None:
770
+ generation_stats.record(family, model_name)
771
+ return result
772
+
773
+
774
+ def generate_image_ui(
775
+ model_name: str,
776
+ vae_name: str | None,
777
+ prompt: str,
778
+ negative_prompt: str,
779
+ width: int,
780
+ height: int,
781
+ seed: int,
782
+ steps: int,
783
+ cfg: float,
784
+ sampler_name: str,
785
+ scheduler: str,
786
+ enable_lora: bool,
787
+ lora_name: str,
788
+ lora_strength: float,
789
+ enable_lora_2: bool,
790
+ lora_name_2: str,
791
+ lora_strength_2: float,
792
+ enable_lora_3: bool,
793
+ lora_name_3: str,
794
+ lora_strength_3: float,
795
+ enable_lora_4: bool,
796
+ lora_name_4: str,
797
+ lora_strength_4: float,
798
+ embedding_enable_1: bool = False,
799
+ embedding_enable_2: bool = False,
800
+ embedding_enable_3: bool = False,
801
+ embedding_enable_4: bool = False,
802
+ progress: gr.Progress = gr.Progress(track_tqdm=False),
803
+ ) -> tuple[np.ndarray, str]:
804
+ img_np, base_info, effective_prompt, effective_negative_prompt = serialized_generate(
805
+ model_name,
806
+ prompt,
807
+ negative_prompt,
808
+ width,
809
+ height,
810
+ seed,
811
+ steps,
812
+ cfg,
813
+ sampler_name,
814
+ scheduler,
815
+ enable_lora,
816
+ lora_name,
817
+ lora_strength,
818
+ enable_lora_2,
819
+ lora_name_2,
820
+ lora_strength_2,
821
+ enable_lora_3,
822
+ lora_name_3,
823
+ lora_strength_3,
824
+ enable_lora_4,
825
+ lora_name_4,
826
+ lora_strength_4,
827
+ [
828
+ name
829
+ for enabled, name in zip(
830
+ (embedding_enable_1, embedding_enable_2, embedding_enable_3, embedding_enable_4),
831
+ [item[0] for item in ILLUSTRIOUS_EMBEDDING_FILES],
832
+ )
833
+ if enabled
834
+ ],
835
+ vae_name,
836
+ progress,
837
+ )
838
+ gui_info = (
839
+ base_info
840
+ + f"\n**Effective Prompt:** `{effective_prompt}`"
841
+ + f"\n**Effective Negative Prompt:** `{effective_negative_prompt}`"
842
+ )
843
+ return img_np, gui_info
844
+
845
+
846
+ def generate_image(
847
+ model_name: str,
848
+ prompt: str,
849
+ negative_prompt: str,
850
+ width: int,
851
+ height: int,
852
+ seed: int,
853
+ steps: int,
854
+ cfg: float,
855
+ sampler_name: str,
856
+ scheduler: str,
857
+ enable_lora: bool,
858
+ lora_name: str,
859
+ lora_strength: float,
860
+ enable_lora_2: bool,
861
+ lora_name_2: str,
862
+ lora_strength_2: float,
863
+ enable_lora_3: bool,
864
+ lora_name_3: str,
865
+ lora_strength_3: float,
866
+ enable_lora_4: bool,
867
+ lora_name_4: str,
868
+ lora_strength_4: float,
869
+ progress: gr.Progress = gr.Progress(track_tqdm=False),
870
+ ) -> tuple[np.ndarray, str]:
871
+ img_np, base_info, _effective_prompt, _effective_negative_prompt = serialized_generate(
872
+ model_name,
873
+ prompt,
874
+ negative_prompt,
875
+ width,
876
+ height,
877
+ seed,
878
+ steps,
879
+ cfg,
880
+ sampler_name,
881
+ scheduler,
882
+ enable_lora,
883
+ lora_name,
884
+ lora_strength,
885
+ enable_lora_2,
886
+ lora_name_2,
887
+ lora_strength_2,
888
+ enable_lora_3,
889
+ lora_name_3,
890
+ lora_strength_3,
891
+ enable_lora_4,
892
+ lora_name_4,
893
+ lora_strength_4,
894
+ None,
895
+ None,
896
+ progress,
897
+ )
898
+ return img_np, base_info
899
+
900
+
901
+ # --------------------------------------------------------------------------
902
+ # UI
903
+ # --------------------------------------------------------------------------
904
+ RESOLUTIONS = {
905
+ "1:1 (1024x1024)": (1024, 1024),
906
+ "2:3 (832x1216)": (832, 1216),
907
+ "3:2 (1216x832)": (1216, 832),
908
+ "3:4 (896x1152)": (896, 1152),
909
+ "4:3 (1152x896)": (1152, 896),
910
+ "9:16 (768x1344)": (768, 1344),
911
+ "16:9 (1344x768)": (1344, 768),
912
+ }
913
+
914
+ SAMPLERS = ["er_sde", "euler", "euler_ancestral", "dpmpp_2m", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_sde", "heun", "ddim", "lcm", "uni_pc"]
915
+ SCHEDULERS = ["simple", "normal", "karras", "exponential", "sgm_uniform"]
916
+
917
+ ANIMA_LORA_CHOICES = [name for name, _ in LORA_FILES] + list(JUJO_ENTRIES["anima"])
918
+ ILLUSTRIOUS_LORA_CHOICES = [name for name, _, _ in ILLUSTRIOUS_LORA_FILES] + list(JUJO_ENTRIES["illustrious"])
919
+ ALL_LORA_CHOICES = ANIMA_LORA_CHOICES + ILLUSTRIOUS_LORA_CHOICES
920
+
921
+ ANIMA_PROMPT = "masterpiece, best quality, score_9, score_8, score_7,"
922
+ ANIMA_NEGATIVE = (
923
+ "worst quality, low quality, score_1, score_2, score_3, artist name, "
924
+ "blurry, jpeg artifacts, chromatic aberration"
925
+ )
926
+ ANIMA_USAGE_INFO = (
927
+ "## Animaの初期値と使い分け\n\n"
928
+ "### 現在の初期値\n\n"
929
+ "- **既定Checkpoint:** `anima-turbo-v1.1.safetensors`\n"
930
+ "- **Steps:** 8\n"
931
+ "- **CFG:** 1.0\n"
932
+ "- **Sampler / Scheduler:** `er_sde` / `simple`\n"
933
+ "- **LoRA:** 初期状態ではすべて無効。LoRA 1はP3 Aesthetics Details 0.8、"
934
+ "LoRA 2はGPT-Image Anima Base 0.8を選択済み\n"
935
+ "- **以前の初期設定を使う場合:** 「LoRA設定・追加モデル」を開き、"
936
+ "LoRA 1・2のEnableを手動で有効にしてください(どちらもStrength 0.8)。\n\n"
937
+ "既定CheckpointがTurbo版のため、初期値は短いステップ数を優先した"
938
+ "公式Turbo設定です。下の生成プリセットは「Turboモデル(公式)」が既定です。\n\n"
939
+ "### 推奨プリセット\n\n"
940
+ "| 使用するCheckpoint | 推奨プリセット | 理由 |\n"
941
+ "| --- | --- | --- |\n"
942
+ "| `anima-turbo-v1.1.safetensors` | **Turboモデル(公式)** | Turbo用Checkpointのため、8 steps / CFG 1.0をそのまま使います。 |\n"
943
+ "| `anima-turbo-v1.1.safetensors`以外のCheckpoint | **通常(非Turbo)** | まず30 steps / CFG 4.0でモデル本来の品質を確認します。名前にTurboを含むbaseNoTurbo版もここに含まれます。 |\n"
944
+ "| 非Turboモデルで短時間生成を試す場合 | **非Turbo+Turbo LoRA** | 8 steps / CFG 1.0を試せますが、崩れやディテール不足が出る可能性があります。 |\n\n"
945
+ "迷った場合は、Turboモデルには「Turboモデル(公式)」、非Turboモデルには「通常(非Turbo)」を選択してください。"
946
+ "「非Turbo+Turbo LoRA」は、まず通常設定と比較しながら使用してください。\n\n"
947
+ "Checkpointを変更してもプリセットは自動で切り替わりません。Turbo版には「Turboモデル(公式)」、"
948
+ "それ以外のCheckpointには「通常(非Turbo)」を選択してください。\n\n"
949
+ "### 非TurboモデルでTurbo-ANIMA-v2 LoRAを使う場合\n\n"
950
+ "- **Steps:** 8\n"
951
+ "- **CFG:** 1.0から開始\n"
952
+ "- **Sampler / Scheduler:** `er_sde` / `simple`\n"
953
+ "- **Turbo-ANIMA-v2:** Strength 1.0から開始\n"
954
+ "- Turbo LoRAを使う場合は、まず短いステップ数で比較し、"
955
+ "崩れやディテール不足があればStrengthを下げるか通常設定へ戻してください。\n\n"
956
+ "### 非Turboモデルを通常設定で使う場合\n\n"
957
+ "Anima公式READMEに記載された通常生成の参考値は、"
958
+ "**30 steps / CFG 4 / `er_sde`・`simple`**です。"
959
+ "この場合はTurbo-ANIMA-v2などのTurbo LoRAを無効にして比較してください。"
960
+ )
961
+
962
+ ILLUSTRIOUS_USAGE_INFO = (
963
+ "## Illustriousの初期値と使い分け\n\n"
964
+ "### 現在の初期値\n\n"
965
+ "- **既定Checkpoint:** `waiIllustriousSDXL_v170.safetensors`\n"
966
+ "- **既定プリセット:** WAI標準(高品質)\n"
967
+ "- **解像度:** 1:1(1024×1024)\n"
968
+ "- **Steps / CFG:** 30 / 5.0\n"
969
+ "- **Sampler / Scheduler:** `euler_ancestral` / `normal`\n"
970
+ "- **VAE:** Checkpoint内蔵VAE\n"
971
+ "- **Embedding:** 初期状態はすべて無効。必要な項目を選択可能\n\n"
972
+ "Illustriousでは通常品質を優先するWAI標準と、短いステップ数で生成するDMD2高速を"
973
+ "別プリセットとして用意しています。上の「生成方式」で切り替えてください。\n\n"
974
+ "### 推奨プリセット\n\n"
975
+ "| 用途 | 推奨プリセット | 設定の目安 |\n"
976
+ "| --- | --- | --- |\n"
977
+ "| 品質優先・通常生成(すべてのIllustrious系Checkpointの開始点) | **WAI標準(高品質)** | 30 steps / CFG 5.0 / `euler_ancestral` / `normal` / DMD2無効 |\n"
978
+ "| 速度優先・短時間生成 | **DMD2高速(8-step)** | 8 steps / CFG 1.6 / `lcm` / `normal` / DMD2 LoRA 0.7 |\n\n"
979
+ "Checkpointを変更した場合も、まず上記プリセットから開始してください。モデルカードに固有の推奨値がある場合のみ、Steps・CFG・LoRAを調整します。\n\n"
980
+ "### WAI標準(通常生成)\n\n"
981
+ "- **Steps / CFG:** 30 / 5.0\n"
982
+ "- **Sampler / Scheduler:** `euler_ancestral` / `normal`\n"
983
+ "- **DMD2 LoRA:** 無効\n"
984
+ "- **Prompt:** `masterpiece, best quality, amazing quality,` を初期値として使用\n"
985
+ "- **Negative:** 品質・人体破綻・文字を抑える基本タグを初期値として使用\n\n"
986
+ "> **注意:** CFGを高くしすぎたり、Negative側のタグを強く重み付けしすぎると、"
987
+ "彩度・輪郭・構図が崩れることがあります。まずCFG 5.0と重み付けなしのNegativeから調整してください。\n\n"
988
+ "通常のWAI設定では品質と安定性を優先します。まずこのプリセットでCheckpointごとの"
989
+ "出力を確認し、必要に応じてプロンプトと解像度を調整してください。\n\n"
990
+ "### DMD2高速(8-step)\n\n"
991
+ "- **Steps / CFG:** 8 / 1.6(目安 1.4~1.8)\n"
992
+ "- **Sampler / Scheduler:** `lcm` / `normal`\n"
993
+ "- **LoRA 4:** `dmd2_sdxl_4step_lora.safetensors`、Strength 0.7\n"
994
+ "- **他の高速化LoRA:** Hyper-SDXLなどは同時に有効化しない\n\n"
995
+ "DMD2は短いステップ数と低めのCFGで使う専用設定です。WAI標準と同じSteps・CFGのまま"
996
+ "LoRAだけを有効にする使い方は避け、プリセットで一式を切り替えてください。\n\n"
997
+ "### VAE\n\n"
998
+ "Illustriousページでは、Checkpoint内蔵VAEと2種類の外部SDXL VAEを選択できます。"
999
+ "初期値はCheckpoint内蔵VAEです。VAEは最終的な画像の復号に使われ、色味・明暗・輪郭に"
1000
+ "差が出る場合があります。同じSeed・Prompt・生成設定のままVAEだけを変更して比較してください。\n\n"
1001
+ "- **Checkpoint内蔵VAE(初期値):** 各Checkpointに含まれるVAEを使用\n"
1002
+ "- **`sdxl_vae.safetensors`:** Stability AIのSDXL VAEを明示的に使用\n"
1003
+ "- **`illustriousXLV10_v10.safetensors`:** Illustrious XL v1.0向け。"
1004
+ "過度な彩度や色ずれを避けたい場合の比較候補\n\n"
1005
+ "SD1.5向けVAEはIllustrious / SDXLと互換ではないため、この一覧には表示しません。\n\n"
1006
+ "### Embeddings\n\n"
1007
+ "IllustriousページのEmbeddingは初期状態ではすべて無効です。必要な項目を選択すると、"
1008
+ "ComfyUIの`embedding:ファイル名`としてNegative promptの先頭に追加されます。"
1009
+ "Checkpointと同じSDXL / Illustrious系のEmbeddingだけを使用してください。\n\n"
1010
+ "| Embedding | 初期状態 | 対象 |\n"
1011
+ "| --- | --- | --- |\n"
1012
+ "| `negativeXL_D.safetensors` | **無効(選択可能)** | SDXL / Illustrious向け |\n"
1013
+ "| `negativeXL.safetensors` | 無効(選択可能) | SDXL向け |\n"
1014
+ "| `EasyNegativeV2.safetensors` | 無効(選択可能) | SD1.5向け・Illustriousでは非推奨 |\n"
1015
+ "| `bad-hands-5.pt` | 無効(選択可能) | SD1.5向け・Illustriousでは非推奨 |\n\n"
1016
+ "EmbeddingはNegative promptにだけ適用され、Positive promptやCheckpointは変更しません。"
1017
+ "手動で変更した値は、次にプリセットを選択するまで維持されます。"
1018
+ )
1019
+
1020
+ ANIMA_CREDITS = (
1021
+ "### Checkpoint\n\n"
1022
+ "- `anima-turbo-v1.1.safetensors`、`anima-aesthetic-v1.1.safetensors` — "
1023
+ "[CircleStone Labs / Anima](https://huggingface.co/circlestone-labs/Anima)(CircleStone Labs / Comfy Org)\n"
1024
+ "- `fnMixAnimaTurbo_baseNoTurbo.safetensors` — "
1025
+ "[Fn-Mix Anima-Turbo(Civitai)](https://civitai.com/models/2719904)\n"
1026
+ "- `tekitoMix_v32.safetensors` — "
1027
+ "[Tekito-Mix v3.2(Civitai)](https://civitai.com/models/2605979/tekito-mix)\n\n"
1028
+ "### LoRA\n\n"
1029
+ "- `Anima-P3-Aesthetics-Details_v1.0.safetensors` — "
1030
+ "[Anima-P3-Aesthetics(RunningHub・確認できた配布ページ)](https://www.runninghub.ai/model/public/2061426010008150017)(ページ表示名: Openclaw2026)\n"
1031
+ "- `anima-base-1-masterpiece-v51.safetensors` — "
1032
+ "[Aesthetic Quality Modifiers - Masterpiece(Civitai)](https://civitai.com/models/929497/aesthetic-quality-modifiers-masterpiece)\n"
1033
+ "- `anima-highres-aesthetic-boost.safetensors` — "
1034
+ "[Anima Highres/Aesthetic Boost(Civitai)](https://civitai.com/models/2540444/anima-highresaesthetic-boost)\n"
1035
+ "- `gpt-image-2_anima-base1_v1.safetensors` — "
1036
+ "[Style Greater - GPT Image 2(Civitai)](https://civitai.com/models/2564225/style-greater-gpt-image-2)\n"
1037
+ "- `anima-turbo-lora-v0.2.safetensors` — "
1038
+ "[Anima Official LoRAs(Hugging Face)](https://huggingface.co/circlestone-labs/Anima-Official-LoRAs)\n"
1039
+ "- `Turbo-ANIMA-v2.safetensors` — "
1040
+ "[Turbo ANIMA v2(Civitai)](https://civitai.com/models/2619830?modelVersionId=3039549)\n\n"
1041
+ "### 共通コンポーネント\n\n"
1042
+ "- `qwen_3_06b_base.safetensors`、`qwen_image_vae.safetensors` — "
1043
+ "[CircleStone Labs / Anima](https://huggingface.co/circlestone-labs/Anima)\n"
1044
+ "PrivateコレクションはSpaceの取得用ミラーです。以下は原作者または公式配布元へのリンクです。\n\n"
1045
+ "配布条件・ライセンス・原作者の詳細は、各配布元のモデルカードを確認してください。"
1046
+ )
1047
+
1048
+ ILLUSTRIOUS_CREDITS = (
1049
+ "### Checkpoint\n\n"
1050
+ "- `waiIllustriousSDXL_v170.safetensors` — "
1051
+ "[WAI0731 / WAI-illustrious-SDXL v17.0(Civitai)](https://civitai.com/models/827184/wai-illustrious-sdxl)\n"
1052
+ "- `kiwimixXL_v3.safetensors` — "
1053
+ "[KiwiMix XL v3(Civitai)](https://civitai.com/models/264709?modelVersionId=1075361)\n"
1054
+ "- `mhillustrious_v10.safetensors` — "
1055
+ "[mHIllustrious(Civitai)](https://civitai.com/models/1586012/mhillustrious?modelVersionId=1794770)\n"
1056
+ "- `zukiAnimeILL_v50.safetensors` — "
1057
+ "[zukiAnimeILL v5.0(Civitai)](https://civitai.com/models/949488?modelVersionId=1765723)\n\n"
1058
+ "### LoRA\n\n"
1059
+ "- `Detail_Tweaker_Illustrious_BSY_V3.safetensors` — "
1060
+ "[Detail Tweaker Illustrious By Stable Yogi v3.0(Civitai)](https://civitai.com/models/1016670/detailtweakerillustriousbystableyogi)\n"
1061
+ "- `Hyper-SDXL-8steps-CFG-lora.safetensors` — "
1062
+ "[ByteDance / Hyper-SD](https://huggingface.co/ByteDance/Hyper-SD)(ByteDance Hyper-SD team)\n"
1063
+ "- `dmd2_sdxl_4step_lora.safetensors` — "
1064
+ "[DMD2 Speed LoRA / SDXL Pony・Illustrious向け配布ページ(Civitai)](https://civitai.com/models/1608870/dmd2-speed-lora-sdxl-pony-illustrious?modelVersionId=1820705)\n\n"
1065
+ "### VAE\n\n"
1066
+ "- `sdxl_vae.safetensors` — "
1067
+ "[Stability AI / SDXL VAE(Hugging Face)](https://huggingface.co/stabilityai/sdxl-vae)\n"
1068
+ "- `illustriousXLV10_v10.safetensors` — "
1069
+ "[Illustrious XL v1.0 VAE(Civitai)](https://civitai.com/models/1693029/illustrious-xl-v10?modelVersionId=1916060)\n\n"
1070
+ "### Embeddings\n\n"
1071
+ "- `negativeXL_D.safetensors`、`negativeXL.safetensors` — "
1072
+ "[negativeXL D(Civitai)](https://civitai.com/models/118418?modelVersionId=134583)\n"
1073
+ "- `EasyNegativeV2.safetensors` — "
1074
+ "[EasyNegativeV2(Civitai)](https://civitai.com/models/100191/ti-easynegativev2-textual-inversion-embedding)\n"
1075
+ "- `bad-hands-5.pt` — "
1076
+ "[bad-hands-5(Civitai)](https://civitai.com/models/116230?modelVersionId=125849)\n\n"
1077
+ "PrivateコレクションはSpaceの取得用ミラーです。以下は原作者または公式配布元へのリンクです。\n\n"
1078
+ "配布条件・ライセンス・原作者の詳細は、各配布元のモデルカードを確認してください。"
1079
+ )
1080
+
1081
+ PROJECT_CREDITS = (
1082
+ "### アプリのベース・拡張について\n\n"
1083
+ "**Anima Collection+ MCP** は、**Anima Collection** をベースにしたフォークです。"
1084
+ "まずMCPサーバ機能を追加し、その後Illustrious対応��複数モデル対応・お気に入りなどを拡張しました。"
1085
+ "「+」は元のAnima Collectionからの機能拡張を表します。\n\n"
1086
+ )
1087
+ ANIMA_CREDITS = PROJECT_CREDITS + ANIMA_CREDITS + jujo_credits("anima")
1088
+ ILLUSTRIOUS_CREDITS = PROJECT_CREDITS + ILLUSTRIOUS_CREDITS + jujo_credits("illustrious")
1089
+
1090
+ ANIMA_PRESETS = {
1091
+ "turbo": {
1092
+ "label": "Turboモデル(公式)",
1093
+ "steps": 8,
1094
+ "cfg": 1.0,
1095
+ "sampler": "er_sde",
1096
+ "scheduler": "simple",
1097
+ "lora4_enable": False,
1098
+ "lora4_file": "Turbo-ANIMA-v2.safetensors",
1099
+ "lora4_strength": 1.0,
1100
+ "status": (
1101
+ "**適用中: Turboモデル(公式)**\n\n"
1102
+ "`anima-turbo-v1.1`向けの公式推奨に合わせた設定です。"
1103
+ "8 steps / CFG 1.0 / er_sde / simple、Turbo-ANIMA-v2は無効です。"
1104
+ ),
1105
+ },
1106
+ "turbo_lora": {
1107
+ "label": "非Turbo+Turbo LoRA",
1108
+ "steps": 8,
1109
+ "cfg": 1.0,
1110
+ "sampler": "er_sde",
1111
+ "scheduler": "simple",
1112
+ "lora4_enable": True,
1113
+ "lora4_file": "Turbo-ANIMA-v2.safetensors",
1114
+ "lora4_strength": 1.0,
1115
+ "status": (
1116
+ "**適用中: 非Turbo+Turbo LoRA**\n\n"
1117
+ "BaseまたはbaseNoTurbo系CheckpointでTurbo LoRAを試す設定です。"
1118
+ "8 steps / CFG 1.0 / er_sde / simple / Turbo-ANIMA-v2 1.0を適用します。"
1119
+ "公式Turboモデルとの併用は想定していません。"
1120
+ ),
1121
+ },
1122
+ "standard": {
1123
+ "label": "通常(非Turbo)",
1124
+ "steps": 30,
1125
+ "cfg": 4.0,
1126
+ "sampler": "er_sde",
1127
+ "scheduler": "simple",
1128
+ "lora4_enable": False,
1129
+ "lora4_file": "Turbo-ANIMA-v2.safetensors",
1130
+ "lora4_strength": 1.0,
1131
+ "status": (
1132
+ "**適用中: Anima通常設定**\n\n"
1133
+ "非Turboモデルの比較用設定です。"
1134
+ "30 steps / CFG 4.0 / er_sde / simpleにし、Turbo-ANIMA-v2を無効にしています。"
1135
+ ),
1136
+ },
1137
+ }
1138
+
1139
+ # WAI-illustrious-SDXL v17's published quality-tag and negative-tag defaults.
1140
+ # Keep the content prompt open for the user's own Danbooru tags instead of
1141
+ # forcing a subject such as ``1girl, solo`` into every generation.
1142
+ ILLUSTRIOUS_PROMPT = "masterpiece, best quality, amazing quality,"
1143
+ ILLUSTRIOUS_NEGATIVE = (
1144
+ "bad quality,worst quality,worst detail,sketch,censor,\n"
1145
+ "bad hands, extra arms, extra hands, extra fingers, long body, logo, emblem, text,"
1146
+ )
1147
+
1148
+ ILLUSTRIOUS_PRESETS = {
1149
+ "wai": {
1150
+ "label": "WAI標準(高品質)",
1151
+ "steps": 30,
1152
+ "cfg": 5.0,
1153
+ "sampler": "euler_ancestral",
1154
+ "scheduler": "normal",
1155
+ "lora4_enable": False,
1156
+ "lora4_file": "dmd2_sdxl_4step_lora.safetensors",
1157
+ "lora4_strength": 0.7,
1158
+ "status": (
1159
+ "**適用中: WAI標準**\n\n"
1160
+ "WAI Illustrious v17の通常生成向け設定です。DMD2 LoRAは無効です。"
1161
+ ),
1162
+ },
1163
+ "dmd2": {
1164
+ "label": "DMD2高速(8-step)",
1165
+ "steps": 8,
1166
+ "cfg": 1.6,
1167
+ "sampler": "lcm",
1168
+ "scheduler": "normal",
1169
+ "lora4_enable": True,
1170
+ "lora4_file": "dmd2_sdxl_4step_lora.safetensors",
1171
+ "lora4_strength": 0.7,
1172
+ "status": (
1173
+ "**適用中: DMD2高速**\n\n"
1174
+ "DMD2の推奨範囲(8 Steps / CFG 1.4~1.8 / LCM / LoRA 0.7)を適用しています。"
1175
+ ),
1176
+ },
1177
+ }
1178
+
1179
+
1180
+ def build_generation_page(
1181
+ *,
1182
+ family_label: str,
1183
+ model_choices: list[str],
1184
+ lora_choices: list[str],
1185
+ prompt_default: str,
1186
+ negative_default: str,
1187
+ steps_default: int,
1188
+ cfg_default: float,
1189
+ sampler_default: str,
1190
+ scheduler_default: str,
1191
+ lora_defaults: list[tuple[bool, str, float]],
1192
+ resolution_default: str = "1:1 (1024x1024)",
1193
+ width_default: int = 1024,
1194
+ height_default: int = 1024,
1195
+ preset_specs: dict[str, dict] | None = None,
1196
+ preset_default: str | None = None,
1197
+ embedding_files: list[tuple[str, str, str]] | None = None,
1198
+ embedding_defaults: list[bool] | None = None,
1199
+ embedding_interactive: bool = True,
1200
+ vae_choices: list[tuple[str, str]] | None = None,
1201
+ vae_default: str | None = None,
1202
+ ):
1203
+ """Build one private WebUI page with family-specific components."""
1204
+ lora_available = bool(lora_choices)
1205
+ family = family_label.lower()
1206
+ dropdown_choices = [("---(未選択・適用しない)", NONE_LORA)] + [
1207
+ (jujo_label(family, name), name) for name in lora_choices
1208
+ ]
1209
+ preset_input = None
1210
+ preset_status = None
1211
+
1212
+ # Keep the storage bridge mounted even when its native <details> is shut.
1213
+ # Gradio Accordions lazily mount children and would delay restoration.
1214
+ settings_panel = gr.Column()
1215
+
1216
+ if preset_specs:
1217
+ gr.Markdown("### 生成プリセット")
1218
+ preset_input = gr.Radio(
1219
+ label="生成方式",
1220
+ choices=[(spec["label"], key) for key, spec in preset_specs.items()],
1221
+ value=preset_default or next(iter(preset_specs)),
1222
+ )
1223
+ preset_status = gr.Markdown(
1224
+ preset_specs[preset_default or next(iter(preset_specs))]["status"]
1225
+ + "\n\n_手動変更した値は、次にプリセットを選択するまで維持されます。_"
1226
+ )
1227
+
1228
+ with gr.Row():
1229
+ with gr.Column(scale=1):
1230
+ model_input = gr.Dropdown(
1231
+ label=f"{family_label} Checkpoint",
1232
+ choices=model_choices,
1233
+ value=model_choices[0],
1234
+ )
1235
+ if vae_choices:
1236
+ vae_input = gr.Dropdown(
1237
+ label="VAE",
1238
+ choices=vae_choices,
1239
+ value=vae_default or vae_choices[0][1],
1240
+ info=(
1241
+ "初期値はCheckpoint内蔵VAEです。色味を比較する場合のみ"
1242
+ "外部SDXL VAEへ切り替えてください。"
1243
+ ),
1244
+ )
1245
+ else:
1246
+ vae_input = gr.State(value=None)
1247
+ prompt_input = prompt_with_completion(
1248
+ label="Prompt",
1249
+ lines=3,
1250
+ value=prompt_default,
1251
+ elem_id=f"{family_label.lower()}-positive-prompt",
1252
+ )
1253
+ negative_input = prompt_with_completion(
1254
+ label="Negative prompt",
1255
+ lines=2,
1256
+ value=negative_default,
1257
+ elem_id=f"{family_label.lower()}-negative-prompt",
1258
+ )
1259
+ if family_label == "Illustrious":
1260
+ gr.Markdown(
1261
+ "> **注意:** CFGを高くしすぎたり、Negative側のタグを強く重み付けしすぎると、"
1262
+ "画像の彩度・輪郭・構図が崩れることがあります。まずCFG 5.0と"
1263
+ "重み付けなしのNegativeから調整してください。"
1264
+ )
1265
+ embedding_components = []
1266
+ if embedding_files:
1267
+ gr.Markdown(
1268
+ "### Negative Embeddings"
1269
+ + ("(現在は無効)" if not embedding_interactive else "")
1270
+ )
1271
+ gr.Markdown(
1272
+ (
1273
+ "検証中のため現在はすべて無効化されています。"
1274
+ "再有効化するまでNegative promptには追加されません。"
1275
+ if not embedding_interactive
1276
+ else "選択したEmbeddingはNegative promptの先頭に自動適用されます。"
1277
+ "Checkpoint系統が一致するものだけを有効にしてください。"
1278
+ )
1279
+ )
1280
+ defaults = embedding_defaults or [False] * len(embedding_files)
1281
+ for index, (embedding_name, _subfolder, family_label_text) in enumerate(
1282
+ embedding_files
1283
+ ):
1284
+ available = os.path.isfile(EMBEDDING_PATHS.get(embedding_name, ""))
1285
+ embedding_components.append(
1286
+ gr.Checkbox(
1287
+ label=embedding_name,
1288
+ info=f"系統: {family_label_text}",
1289
+ value=bool(defaults[index]) and available and embedding_interactive,
1290
+ interactive=embedding_interactive and available,
1291
+ )
1292
+ )
1293
+ resolution = gr.Dropdown(
1294
+ label="Resolution",
1295
+ choices=list(RESOLUTIONS),
1296
+ value=resolution_default,
1297
+ )
1298
+ with gr.Row():
1299
+ width_input = gr.Slider(512, 1536, value=width_default, step=16, label="Width")
1300
+ height_input = gr.Slider(512, 1536, value=height_default, step=16, label="Height")
1301
+ with gr.Row():
1302
+ seed_input = gr.Number(label="Seed (-1 = random)", value=-1, precision=0)
1303
+ steps_input = gr.Slider(
1304
+ label="Steps", minimum=6, maximum=60, value=steps_default, step=1
1305
+ )
1306
+ cfg_input = gr.Slider(
1307
+ label="CFG", minimum=1.0, maximum=8.0, value=cfg_default, step=0.1
1308
+ )
1309
+ with gr.Accordion("Sampler", open=False):
1310
+ sampler_input = gr.Dropdown(
1311
+ label="Sampler", choices=SAMPLERS, value=sampler_default
1312
+ )
1313
+ scheduler_input = gr.Dropdown(
1314
+ label="Scheduler", choices=SCHEDULERS, value=scheduler_default
1315
+ )
1316
+ with gr.Accordion("LoRA設定・追加モデル(最大4つ)", open=False, elem_classes=["lora-accordion"]):
1317
+ gr.Markdown(
1318
+ "**画風・目・表情などを調整するLoRAを選択できます。** "
1319
+ "`---`は未選択として扱い、適用しません。"
1320
+ "JujoHotaruの追加LoRAは、初回生成時に選択したファイルを取得します。"
1321
+ )
1322
+ if not lora_available:
1323
+ gr.Markdown(
1324
+ f"No {family_label} LoRA is registered. Add compatible files to "
1325
+ "the family configuration before enabling a slot."
1326
+ )
1327
+ lora_components = []
1328
+ lora_enable_inputs = []
1329
+ lora_name_inputs = []
1330
+ lora_strength_inputs = []
1331
+ for slot_index, (enabled, name, strength) in enumerate(lora_defaults, start=1):
1332
+ with gr.Row():
1333
+ enable = gr.Checkbox(
1334
+ label=f"LoRA {slot_index} Enable",
1335
+ value=lora_selected(enabled, name) and lora_available,
1336
+ interactive=lora_available and name != NONE_LORA,
1337
+ )
1338
+ lora_name = gr.Dropdown(
1339
+ label=f"LoRA {slot_index} File",
1340
+ choices=dropdown_choices,
1341
+ value=name if name in lora_choices else NONE_LORA,
1342
+ interactive=lora_available,
1343
+ )
1344
+ lora_strength = gr.Slider(
1345
+ label=f"LoRA {slot_index} Strength",
1346
+ minimum=0.0,
1347
+ maximum=2.0,
1348
+ value=strength,
1349
+ step=0.05,
1350
+ interactive=lora_available and name != NONE_LORA,
1351
+ )
1352
+ def update_lora_selection(selected, enabled):
1353
+ selected_file = bool(selected and selected != NONE_LORA)
1354
+ return (
1355
+ gr.Checkbox(value=bool(enabled and selected_file), interactive=selected_file),
1356
+ gr.Slider(interactive=selected_file),
1357
+ )
1358
+
1359
+ # Only direct user edits should reset an empty slot. A
1360
+ # programmatic sample/preset update sets all three fields
1361
+ # together and must not be overwritten by this callback.
1362
+ lora_name.input(
1363
+ update_lora_selection,
1364
+ inputs=[lora_name, enable],
1365
+ outputs=[enable, lora_strength],
1366
+ queue=False,
1367
+ api_visibility="private",
1368
+ )
1369
+ lora_components.extend([enable, lora_name, lora_strength])
1370
+ lora_enable_inputs.append(enable)
1371
+ lora_name_inputs.append(lora_name)
1372
+ lora_strength_inputs.append(lora_strength)
1373
+ if family == "illustrious":
1374
+ gr.Markdown("`Pony用・ILL未検証` のLoRAは一覧の下部にあります。Illustriousでの効き方・画質・再現性は未検証です。")
1375
+ pony_warning = gr.Markdown(
1376
+ pony_selection_notice(family, [slot[1] for slot in lora_defaults]),
1377
+ elem_classes=["pony-selection-warning"],
1378
+ )
1379
+ gr.on(
1380
+ triggers=[component.change for component in lora_name_inputs],
1381
+ fn=lambda *names: pony_selection_notice(family, names),
1382
+ inputs=lora_name_inputs, outputs=pony_warning,
1383
+ queue=False, trigger_mode="always_last", api_visibility="private",
1384
+ show_progress="hidden",
1385
+ )
1386
+ build_jujo_browser(family, lora_components, lora_defaults)
1387
+ generate_btn = gr.Button(f"Generate with {family_label}", variant="primary")
1388
+
1389
+ with gr.Column(scale=1):
1390
+ output_image = gr.Image(
1391
+ label="Generated Image",
1392
+ format="png",
1393
+ streaming=True,
1394
+ )
1395
+ info_output = gr.Markdown(
1396
+ STATIC_INFO
1397
+ + f"\n**Page:** {family_label}"
1398
+ + "\n_Run a generation to see live GPU/VRAM stats._"
1399
+ )
1400
+
1401
+ # Restoration sets all fields atomically; only a direct user selection
1402
+ # should overwrite manually adjusted dimensions or sampling settings.
1403
+ resolution.input(
1404
+ lambda name: list(RESOLUTIONS[name]),
1405
+ inputs=[resolution],
1406
+ outputs=[width_input, height_input],
1407
+ api_visibility="private",
1408
+ )
1409
+ if preset_input is not None and preset_status is not None and preset_specs:
1410
+ def apply_preset(preset_name: str):
1411
+ spec = preset_specs[preset_name]
1412
+ return (
1413
+ spec["steps"],
1414
+ spec["cfg"],
1415
+ spec["sampler"],
1416
+ spec["scheduler"],
1417
+ gr.Checkbox(value=spec["lora4_enable"], interactive=True),
1418
+ gr.Dropdown(value=spec["lora4_file"]),
1419
+ gr.Slider(value=spec["lora4_strength"], interactive=True),
1420
+ spec["status"] + "\n\n_手動変更した値は、次にプリセットを選択するまで維持されます。_",
1421
+ )
1422
+
1423
+ preset_input.input(
1424
+ apply_preset,
1425
+ inputs=[preset_input],
1426
+ outputs=[
1427
+ steps_input,
1428
+ cfg_input,
1429
+ sampler_input,
1430
+ scheduler_input,
1431
+ lora_enable_inputs[3],
1432
+ lora_name_inputs[3],
1433
+ lora_strength_inputs[3],
1434
+ preset_status,
1435
+ ],
1436
+ api_visibility="private",
1437
+ )
1438
+ settings_controls = {
1439
+ "model": model_input, "prompt": prompt_input, "negative_prompt": negative_input,
1440
+ "resolution": resolution, "width": width_input, "height": height_input,
1441
+ "seed": seed_input, "steps": steps_input, "cfg": cfg_input,
1442
+ "sampler": sampler_input, "scheduler": scheduler_input,
1443
+ }
1444
+ if preset_input is not None:
1445
+ settings_controls["preset"] = preset_input
1446
+ if vae_choices:
1447
+ settings_controls["vae"] = vae_input
1448
+ for index in range(4):
1449
+ for suffix, component in zip(("enabled", "file", "strength"), lora_components[index * 3:index * 3 + 3]):
1450
+ settings_controls[f"lora{index + 1}_{suffix}"] = component
1451
+ for index, component in enumerate(embedding_components, 1):
1452
+ settings_controls[f"embedding{index}"] = component
1453
+ with settings_panel:
1454
+ favorite_seed = bind_browser_settings(family, settings_controls, preset_status, preset_specs, info_output)
1455
+ def generate_with_favorite_seed(exact_seed, *args):
1456
+ values = list(args)
1457
+ if exact_seed is not None:
1458
+ values[6] = int(exact_seed)
1459
+ return generate_image_ui(*values)
1460
+
1461
+ generate_btn.click(
1462
+ fn=generate_with_favorite_seed,
1463
+ inputs=[
1464
+ favorite_seed,
1465
+ model_input,
1466
+ vae_input,
1467
+ prompt_input,
1468
+ negative_input,
1469
+ width_input,
1470
+ height_input,
1471
+ seed_input,
1472
+ steps_input,
1473
+ cfg_input,
1474
+ sampler_input,
1475
+ scheduler_input,
1476
+ *lora_components,
1477
+ *embedding_components,
1478
+ ],
1479
+ outputs=[output_image, info_output],
1480
+ show_progress=True,
1481
+ concurrency_id="image_generation",
1482
+ concurrency_limit=1,
1483
+ api_visibility="private",
1484
+ )
1485
+ gr.Markdown(
1486
+ "### 生成件数の集計について\n\n"
1487
+ "モデルの入れ替え・ラインナップ改善の参考にするため、モデルごとの日次生成枚数を集計しています。"
1488
+ "集計対象は生成に成功した画像です。集計データには日付・モード・モデル名・生成枚数のみを記録し、"
1489
+ "プロンプト・生成画像・利用者を識別する情報は含みません。",
1490
+ elem_classes=["generation-count-notice"],
1491
+ )
1492
+
1493
+
1494
+ with gr.Blocks(title="Anima Collection+ MCP") as app:
1495
+ gr.Markdown(
1496
+ "# Anima Collection+ MCP\n"
1497
+ "モデルの系統ごとに、Checkpoint・LoRA・プロンプト・生成設定を分けて管理しています。"
1498
+ )
1499
+
1500
+ # Public API/MCP components are intentionally separate from all visible
1501
+ # tabs. Their types, labels, order, endpoint name, and outputs preserve the
1502
+ # pre-existing generate_image contract while the allowed checkpoints grow.
1503
+ with gr.Group(visible=False):
1504
+ api_model_input = gr.Dropdown(
1505
+ label="Checkpoint", choices=ALL_MODEL_FILES, value=MODEL_FILES[0]
1506
+ )
1507
+ api_prompt_input = gr.Textbox(label="Prompt", value=ANIMA_PROMPT)
1508
+ api_negative_input = gr.Textbox(label="Negative prompt", value=ANIMA_NEGATIVE)
1509
+ api_width_input = gr.Slider(512, 1536, value=1024, step=16, label="Width")
1510
+ api_height_input = gr.Slider(512, 1536, value=1024, step=16, label="Height")
1511
+ api_seed_input = gr.Number(label="Seed (-1 = random)", value=-1, precision=0)
1512
+ api_steps_input = gr.Slider(label="Steps", minimum=6, maximum=60, value=8, step=1)
1513
+ api_cfg_input = gr.Slider(label="CFG", minimum=1.0, maximum=8.0, value=1.0, step=0.1)
1514
+ api_sampler_input = gr.Dropdown(label="Sampler", choices=SAMPLERS, value="er_sde")
1515
+ api_scheduler_input = gr.Dropdown(label="Scheduler", choices=SCHEDULERS, value="simple")
1516
+ api_lora_components = []
1517
+ api_lora_defaults = [
1518
+ (False, "Anima-P3-Aesthetics-Details_v1.0.safetensors", 0.8),
1519
+ (False, "gpt-image-2_anima-base1_v1.safetensors", 0.8),
1520
+ (False, NONE_LORA, 0.8),
1521
+ (False, "Turbo-ANIMA-v2.safetensors", 1.0),
1522
+ ]
1523
+ for slot_index, (enabled, lora_name, strength) in enumerate(
1524
+ api_lora_defaults, start=1
1525
+ ):
1526
+ api_lora_components.extend(
1527
+ [
1528
+ gr.Checkbox(label=f"LoRA {slot_index} Enable", value=enabled),
1529
+ gr.Dropdown(
1530
+ label=f"LoRA {slot_index} File",
1531
+ choices=[NONE_LORA] + ALL_LORA_CHOICES,
1532
+ value=lora_name,
1533
+ ),
1534
+ gr.Slider(
1535
+ label=f"LoRA {slot_index} Strength",
1536
+ minimum=0.0,
1537
+ maximum=2.0,
1538
+ value=strength,
1539
+ step=0.05,
1540
+ ),
1541
+ ]
1542
+ )
1543
+ api_output_image = gr.Image(format="png")
1544
+ api_info_output = gr.Markdown()
1545
+ api_generate_btn = gr.Button("API Generate")
1546
+
1547
+ with gr.Tab("Anima"):
1548
+ with gr.Accordion("Animaモードの使い方はこちら", open=False):
1549
+ gr.Markdown(ANIMA_USAGE_INFO)
1550
+ build_generation_page(
1551
+ family_label="Anima",
1552
+ model_choices=MODEL_FILES,
1553
+ lora_choices=ANIMA_LORA_CHOICES,
1554
+ prompt_default=ANIMA_PROMPT,
1555
+ negative_default=ANIMA_NEGATIVE,
1556
+ steps_default=8,
1557
+ cfg_default=1.0,
1558
+ sampler_default="er_sde",
1559
+ scheduler_default="simple",
1560
+ lora_defaults=[
1561
+ (False, "Anima-P3-Aesthetics-Details_v1.0.safetensors", 0.8),
1562
+ (False, "gpt-image-2_anima-base1_v1.safetensors", 0.8),
1563
+ (False, NONE_LORA, 0.8),
1564
+ (False, "Turbo-ANIMA-v2.safetensors", 1.0),
1565
+ ],
1566
+ preset_specs=ANIMA_PRESETS,
1567
+ preset_default="turbo",
1568
+ )
1569
+ with gr.Accordion("配布元・クレジット", open=False):
1570
+ gr.Markdown(ANIMA_CREDITS)
1571
+
1572
+ with gr.Tab("Illustrious"):
1573
+ with gr.Accordion("Illustriousモードの使い方はこちら", open=False):
1574
+ gr.Markdown(ILLUSTRIOUS_USAGE_INFO)
1575
+ build_generation_page(
1576
+ family_label="Illustrious",
1577
+ model_choices=ILLUSTRIOUS_MODEL_FILES,
1578
+ lora_choices=ILLUSTRIOUS_LORA_CHOICES,
1579
+ prompt_default=ILLUSTRIOUS_PROMPT,
1580
+ negative_default=ILLUSTRIOUS_NEGATIVE,
1581
+ steps_default=30,
1582
+ cfg_default=5.0,
1583
+ sampler_default="euler_ancestral",
1584
+ scheduler_default="normal",
1585
+ lora_defaults=[
1586
+ (False, "Detail_Tweaker_Illustrious_BSY_V3.safetensors", 0.7),
1587
+ (False, NONE_LORA, 0.7),
1588
+ (False, NONE_LORA, 0.7),
1589
+ (False, "dmd2_sdxl_4step_lora.safetensors", 0.7),
1590
+ ],
1591
+ resolution_default="1:1 (1024x1024)",
1592
+ width_default=1024,
1593
+ height_default=1024,
1594
+ preset_specs=ILLUSTRIOUS_PRESETS,
1595
+ preset_default="wai",
1596
+ embedding_files=ILLUSTRIOUS_EMBEDDING_FILES,
1597
+ embedding_defaults=[False, False, False, False],
1598
+ embedding_interactive=True,
1599
+ vae_choices=ILLUSTRIOUS_VAE_CHOICES,
1600
+ vae_default=CHECKPOINT_VAE_OPTION,
1601
+ )
1602
+ with gr.Accordion("配布元・クレジット", open=False):
1603
+ gr.Markdown(ILLUSTRIOUS_CREDITS)
1604
+
1605
+ with gr.Tab("Krea2"):
1606
+ gr.Markdown(
1607
+ "## Krea2\n"
1608
+ "This family has a reserved, separate page so its natural-language "
1609
+ "prompting and 8-step Turbo defaults are never mixed with Anima or "
1610
+ "Illustrious. The Krea2 weights and Qwen3-VL text encoder are not yet "
1611
+ "part of this Space, so generation is disabled here."
1612
+ )
1613
+ with gr.Row():
1614
+ with gr.Column():
1615
+ gr.Dropdown(
1616
+ label="Krea2 Checkpoint",
1617
+ choices=["Krea2 Turbo (not installed)"],
1618
+ value="Krea2 Turbo (not installed)",
1619
+ interactive=False,
1620
+ )
1621
+ gr.Textbox(
1622
+ label="Prompt",
1623
+ lines=4,
1624
+ placeholder="Describe the image in natural language.",
1625
+ interactive=False,
1626
+ )
1627
+ gr.Textbox(label="Negative prompt", value="", interactive=False)
1628
+ with gr.Row():
1629
+ gr.Number(label="Steps", value=8, interactive=False)
1630
+ gr.Number(label="CFG", value=1.0, interactive=False)
1631
+ gr.Number(label="Resolution", value=1024, interactive=False)
1632
+ gr.Button("Krea2 is not configured", interactive=False)
1633
+ with gr.Column():
1634
+ gr.Markdown(
1635
+ "**Planned isolated assets**\n\n"
1636
+ "- Krea2 checkpoint\n"
1637
+ "- Qwen3-VL text encoder\n"
1638
+ "- Krea2-only LoRAs\n"
1639
+ "- Natural-language prompt and Turbo sampling defaults"
1640
+ )
1641
+
1642
+ build_admin_statistics(app, generation_stats, {"anima": MODEL_FILES, "illustrious": ILLUSTRIOUS_MODEL_FILES})
1643
+
1644
+ api_generate_btn.click(
1645
+ fn=generate_image,
1646
+ inputs=[
1647
+ api_model_input,
1648
+ api_prompt_input,
1649
+ api_negative_input,
1650
+ api_width_input,
1651
+ api_height_input,
1652
+ api_seed_input,
1653
+ api_steps_input,
1654
+ api_cfg_input,
1655
+ api_sampler_input,
1656
+ api_scheduler_input,
1657
+ *api_lora_components,
1658
+ ],
1659
+ outputs=[api_output_image, api_info_output],
1660
+ show_progress=True,
1661
+ api_name="generate_image",
1662
+ api_description=(
1663
+ "Generate one image using a registered Anima or Illustrious "
1664
+ "checkpoint, generation settings, and up to four compatible LoRAs."
1665
+ ),
1666
+ concurrency_id="image_generation",
1667
+ concurrency_limit=1,
1668
+ api_visibility="public",
1669
+ )
1670
+
1671
+ generation_stats.start()
1672
+
1673
+ if __name__ == "__main__":
1674
+ app.launch(
1675
+ mcp_server=True,
1676
+ css=Path(__file__).with_name("lora_ui.css").read_text(encoding="utf-8"),
1677
+ )
assets/jujo-browser/browser.css ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ color: var(--body-text-color, #1f2937);
2
+ font-size: 14px;
3
+ container-type: inline-size;
4
+ button, input, select { font: inherit; }
5
+ button, select, input { border: 1px solid var(--border-color-primary, #aab2c2); border-radius: 7px; }
6
+ button, select { background: var(--block-background-fill, white); color: inherit; }
7
+ button { cursor: pointer; min-height: 42px; padding: 8px 12px; }
8
+ button:disabled { opacity: .45; cursor: default; }
9
+ button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid #597dcc; outline-offset: 2px; }
10
+ .jb-toolbar { display: flex; align-items: end; gap: 12px; }
11
+ .jb-category-label { display: none !important; }
12
+ .jb-category-tabs { display: flex; flex-wrap: wrap; gap: 4px; }
13
+ .jb-category-tabs button[aria-pressed="true"] { background: #335bb0; color: white; border-color: #335bb0; }
14
+ .jb-toolbar label, .jb-apply label { display: flex; flex-direction: column; gap: 5px; }
15
+ .jb-search-label { flex: 1; min-width: 0; }
16
+ input, select { min-height: 42px; padding: 8px; background: var(--input-background-fill, white); color: inherit; width: 100%; box-sizing: border-box; }
17
+ .jb-intro, .jb-count { color: var(--body-text-color-subdued, #596273); margin: 8px 0; }
18
+ .jb-layout { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 20px; align-items: start; }
19
+ .jb-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
20
+ @container (max-width: 800px) { .jb-layout { grid-template-columns: minmax(0, 1fr); } }
21
+ @container (max-width: 550px) { .jb-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
22
+ .jb-card { padding: 7px; text-align: left; min-width: 0; overflow: hidden; }
23
+ .jb-card[aria-pressed="true"] { outline: 2px solid #597dcc; background: var(--background-fill-secondary, #eef2ff); }
24
+ .jb-card img { display: block; width: 100%; height: 100px; object-fit: contain; border-radius: 5px; background: var(--background-fill-secondary, #f5f6f8); }
25
+ .jb-crop-shell { display: flex; justify-content: center; width: 100%; height: 100px; }
26
+ .jb-crop-frame { display: block; position: relative; height: 100%; max-width: 100%; overflow: hidden; border-radius: 5px; }
27
+ .jb-card .jb-code { display: block; font-weight: 700; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 5px; }
28
+ .jb-card .jb-title { display: block; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
29
+ .jb-pagination { display: flex; justify-content: center; align-items: center; gap: 16px; margin-top: 12px; }
30
+ .jb-detail { color: inherit; background: var(--block-background-fill, white); border: 1px solid var(--border-color-primary, #aab2c2); border-radius: 9px; box-sizing: border-box; }
31
+ .jb-detail[open] { position: static; display: block; width: 100%; margin: 0; padding: 12px; min-width: 0; max-width: none; }
32
+ .jb-detail-header { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; }
33
+ .jb-close { display: none; }
34
+ .jb-detail h4 { margin: 8px 0; }
35
+ .jb-filename { overflow-wrap: anywhere; font-size: 12px; user-select: text; }
36
+ .jb-preview { width: 100%; border: 0; padding: 0; background: var(--background-fill-secondary, #f5f6f8); }
37
+ .jb-preview img { display: block; width: 100%; height: 190px; object-fit: contain; }
38
+ .jb-preview .jb-crop-shell { height: 190px; }
39
+ .jb-hint, .jb-sample-note { font-size: 12px; margin: 8px 0; }
40
+ .jb-source { color: var(--link-text-color, #245fb0); text-decoration: underline; }
41
+ .jb-original-source { color: var(--link-text-color, #245fb0); text-decoration: underline; font-size: 12px; }
42
+ .jb-apply { border-top: 1px solid var(--border-color-primary, #ddd); margin-top: 12px; padding-top: 12px; display: grid; grid-template-columns: minmax(0, 1fr) 100px; gap: 8px; }
43
+ .jb-apply > label:first-child { grid-column: 1; grid-row: 1; }
44
+ .jb-weight-label { grid-column: 2; grid-row: 1; }
45
+ .jb-slot-summary, .jb-apply-button, .jb-message { grid-column: 1 / -1; }
46
+ .jb-slot-summary { overflow-wrap: anywhere; font-size: 12px; }
47
+ .jb-apply-button { background: #335bb0; color: white; border-color: #335bb0; font-weight: 700; }
48
+ .jb-message { min-height: 1.3em; font-size: 13px; color: var(--body-text-color); }
49
+ .jb-zoom { color: inherit; background: var(--block-background-fill, white); border-radius: 10px; border: 0; padding: 12px; max-width: 96vw; max-height: 94dvh; }
50
+ .jb-zoom[open] { display: flex; flex-direction: column; gap: 8px; }
51
+ .jb-zoom img { max-width: 90vw; max-height: 80dvh; object-fit: contain; }
52
+ .jb-zoom .jb-crop-shell { width: 80vw; height: min(70dvh, 80vw); }
53
+ .jb-zoom-close { align-self: end; }
54
+ dialog::backdrop { background: #0009; }
55
+ @media (max-width: 767px) {
56
+ .jb-layout { display: block; }
57
+ .jb-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
58
+ .jb-card img { height: 90px; }
59
+ .jb-card .jb-crop-shell { height: 90px; }
60
+ .jb-toolbar { gap: 8px; }
61
+ .jb-category-tabs { display: none; }
62
+ .jb-category-label { display: flex !important; width: 100px; flex-shrink: 0; }
63
+ .jb-detail[open] { position: fixed; inset: 0; margin: 0; padding: 12px; width: 100%; height: 100dvh; max-height: 100dvh; border: 0; border-radius: 0; overflow-y: auto; }
64
+ .jb-detail-header { position: sticky; top: -12px; padding: 8px 0; background: var(--block-background-fill, white); z-index: 1; }
65
+ .jb-close { display: block; }
66
+ .jb-preview img { height: 190px; }
67
+ .jb-apply { grid-template-columns: 1fr; }
68
+ .jb-apply > label:first-child, .jb-weight-label { grid-column: auto; grid-row: auto; }
69
+ }
70
+ .jb-pony-badge { display: block; font-size: 11px; color: #986000; font-weight: 700; }
71
+ .jb-compatibility-warning { border-left: 4px solid #b77a00; padding: 10px; background: #b77a0015; line-height: 1.6; }
assets/jujo-browser/browser.html ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="jb-toolbar">
2
+ <label class="jb-category-label">カテゴリ<select class="jb-category">
3
+ <option>すべて</option><option>目の形</option><option>眉</option><option>口</option><option>表情・人物</option><option>効果</option>
4
+ </select></label>
5
+ <div class="jb-category-tabs" role="group" aria-label="カテゴリ"></div>
6
+ <label class="jb-search-label">検索<input class="jb-search" type="search" placeholder="名前・ファイル名・126Aなど"></label>
7
+ </div>
8
+ <p class="jb-intro">JujoHotaru(十条蛍)のサンプル。閲覧だけでは生成設定は変わりません。</p>
9
+ <div class="jb-layout">
10
+ <section class="jb-list" aria-label="LoRAサンプル一覧">
11
+ <p class="jb-count" role="status" aria-live="polite"></p>
12
+ <div class="jb-grid"></div>
13
+ <div class="jb-pagination">
14
+ <button type="button" class="jb-prev">前へ</button><span class="jb-page"></span><button type="button" class="jb-next">次へ</button>
15
+ </div>
16
+ </section>
17
+ <dialog class="jb-detail" aria-label="LoRAの詳細">
18
+ <div class="jb-detail-header"><strong>LoRAの詳細</strong><button type="button" class="jb-close">一覧へ戻る ✕</button></div>
19
+ <div class="jb-detail-content"><p>画像カードを選ぶと、サンプルと適用先の設定を表示します。</p></div>
20
+ </dialog>
21
+ </div>
22
+ <dialog class="jb-zoom" aria-label="サンプル画像の拡大">
23
+ <button type="button" class="jb-zoom-close">閉じる ✕</button><div class="jb-zoom-content"></div>
24
+ </dialog>
assets/jujo-browser/browser.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const entries = props.entries;
2
+ const media = matchMedia("(max-width: 767px)");
3
+ const category = element.querySelector(".jb-category");
4
+ const search = element.querySelector(".jb-search");
5
+ const grid = element.querySelector(".jb-grid");
6
+ const detail = element.querySelector(".jb-detail");
7
+ const content = element.querySelector(".jb-detail-content");
8
+ const zoom = element.querySelector(".jb-zoom");
9
+ let slots = props.value?.slots || [];
10
+ let selected = null, page = 0, pageSize = media.matches ? 4 : 8;
11
+ let target = "", targetOverride = false, returnCard = null, pending = false, pendingTimer;
12
+ let oldOverflow = null;
13
+ const normalize = value => value.toLowerCase().replace(/_/g, " ");
14
+ function filtered() {
15
+ const q = normalize(search.value.trim());
16
+ return entries.filter(entry => (category.value === "すべて" || entry.category === category.value)
17
+ && (!q || normalize(`${entry.name} ${entry.title} ${entry.code} ${entry.compatibility_label || ''}`).includes(q)));
18
+ }
19
+ function defaultTarget() {
20
+ const empty = slots.findIndex(slot => !slot.name || slot.name === "---");
21
+ if (empty >= 0) return String(empty + 1);
22
+ const disabled = slots.findIndex(slot => !slot.enabled);
23
+ return disabled >= 0 ? String(disabled + 1) : "";
24
+ }
25
+ function make(tag, text, className) {
26
+ const node = document.createElement(tag);
27
+ if (text !== undefined) node.textContent = text;
28
+ if (className) node.className = className;
29
+ return node;
30
+ }
31
+ function sampleImage(entry) {
32
+ const img = make("img"); img.src = entry.image; img.alt = `${entry.title} ${entry.code}`;
33
+ img.referrerPolicy = "no-referrer";
34
+ const view = entry.sample_viewport;
35
+ if (!view) return img;
36
+ // CSS framing only: retain the unmodified author-hosted comparison sheet.
37
+ const shell = make("span", undefined, "jb-crop-shell");
38
+ const frame = make("span", undefined, "jb-crop-frame");
39
+ frame.style.aspectRatio = `${view.width} / ${view.height}`;
40
+ Object.assign(img.style, {
41
+ position:"absolute", maxWidth:"none", maxHeight:"none", objectFit:"fill",
42
+ width:`${100 * view.source_width / view.width}%`,
43
+ height:`${100 * view.source_height / view.height}%`,
44
+ left:`${-100 * view.x / view.width}%`, top:`${-100 * view.y / view.height}%`,
45
+ });
46
+ frame.append(img); shell.append(frame); return shell;
47
+ }
48
+ function renderGrid() {
49
+ element.querySelectorAll(".jb-category-tabs button").forEach(button => button.setAttribute("aria-pressed", String(button.textContent === category.value)));
50
+ const list = filtered();
51
+ const pages = Math.max(1, Math.ceil(list.length / pageSize));
52
+ page = Math.max(0, Math.min(page, pages - 1));
53
+ grid.replaceChildren();
54
+ for (const entry of list.slice(page * pageSize, (page + 1) * pageSize)) {
55
+ const card = make("button", undefined, "jb-card");
56
+ card.type = "button"; card.dataset.name = entry.name;
57
+ card.setAttribute("aria-label", `${entry.title} ${entry.code} の詳細を見る`);
58
+ card.setAttribute("aria-pressed", String(selected?.name === entry.name));
59
+ if (entry.image) {
60
+ const sample = sampleImage(entry);
61
+ (sample.matches("img") ? sample : sample.querySelector("img")).loading = "lazy";
62
+ card.append(sample);
63
+ }
64
+ card.append(make("span", entry.code, "jb-code"), make("span", entry.title, "jb-title"));
65
+ if (entry.pony_only) card.append(make("span", entry.compatibility_label, "jb-pony-badge"));
66
+ card.addEventListener("click", () => selectEntry(entry, card));
67
+ grid.append(card);
68
+ }
69
+ if (!list.length) grid.append(make("p", "一致するLoRAがありません。検索条件を変更してください。"));
70
+ element.querySelector(".jb-count").textContent = `${list.length}件 · 1ページ${pageSize}件`;
71
+ element.querySelector(".jb-page").textContent = `${page + 1} / ${pages}ページ`;
72
+ element.querySelector(".jb-prev").disabled = page === 0;
73
+ element.querySelector(".jb-next").disabled = page >= pages - 1;
74
+ }
75
+ function updateTarget() {
76
+ const select = content.querySelector(".jb-target");
77
+ if (!select) return;
78
+ if (!targetOverride) target = defaultTarget();
79
+ select.replaceChildren();
80
+ const empty = make("option", "適用先を選択してください"); empty.value = ""; select.append(empty);
81
+ slots.forEach((slot, index) => {
82
+ const status = !slot.name || slot.name === "---" ? "未選択" : slot.enabled ? "有効" : "無効";
83
+ const option = make("option", `LoRA ${index + 1}(${status})`);
84
+ option.value = String(index + 1); select.append(option);
85
+ });
86
+ select.value = target;
87
+ const current = target ? slots[Number(target) - 1] : null;
88
+ content.querySelector(".jb-slot-summary").textContent = current
89
+ ? `現在:${current.name === "---" ? "未選択" : current.name} / ${current.enabled ? "有効" : "無効"} / 強度 ${current.strength}`
90
+ : "すべてのスロットが有効です。置き換える適用先を選択してください。";
91
+ const button = content.querySelector(".jb-apply-button");
92
+ button.textContent = pending ? "設定中…" : target ? `LoRA ${target}に設定して有効化` : "適用先を選択してください";
93
+ button.disabled = !target || pending;
94
+ }
95
+ function openZoom() {
96
+ zoom.querySelector(".jb-zoom-content").replaceChildren(sampleImage(selected));
97
+ zoom.showModal();
98
+ }
99
+ function renderDetail() {
100
+ content.replaceChildren();
101
+ if (!selected) { content.append(make("p", "画像カードを選ぶと、サンプルと適用先の設定を表示します。")); return; }
102
+ content.append(make("h4", selected.title), make("p", selected.name, "jb-filename"));
103
+ if (selected.pony_only) {
104
+ const warning = make("p", `${selected.compatibility_label}:${selected.compatibility_warning}`, "jb-compatibility-warning");
105
+ warning.setAttribute("role", "note"); content.append(warning);
106
+ }
107
+ if (selected.image) {
108
+ const preview = make("button", undefined, "jb-preview"); preview.type = "button";
109
+ preview.setAttribute("aria-label", "サンプル画像を拡大");
110
+ preview.append(sampleImage(selected));
111
+ preview.addEventListener("click", openZoom); content.append(preview);
112
+ }
113
+ content.append(make("p", selected.sample_note || (selected.group_sample ? "作者のシリーズ共通比較サンプル" : "作者の生成サンプル"), "jb-sample-note"));
114
+ content.append(make("p", selected.hint, "jb-hint"));
115
+ const link = make("a", "作者の使い方・配布元を開く", "jb-source");
116
+ link.href = selected.source; link.target = "_blank"; link.rel = "noopener noreferrer"; content.append(link);
117
+ if (selected.sample_viewport) {
118
+ const original = make("a", "比較画像全体(Original/Type1~6)を開く", "jb-original-source");
119
+ original.href = selected.image; original.target = "_blank"; original.rel = "noopener noreferrer";
120
+ content.append(make("p")); content.append(original);
121
+ }
122
+ const form = make("div", undefined, "jb-apply");
123
+ const targetLabel = make("label", "適用先");
124
+ const targetInput = make("select", undefined, "jb-target"); targetLabel.append(targetInput);
125
+ targetInput.addEventListener("change", () => { target = targetInput.value; targetOverride = true; updateTarget(); });
126
+ const strengthLabel = make("label", "設定する強度", "jb-weight-label");
127
+ const strength = make("input", undefined, "jb-strength");
128
+ strength.type = "number"; strength.min = "0"; strength.max = "2"; strength.step = "0.05";
129
+ strength.value = selected.strength; strengthLabel.append(strength);
130
+ const apply = make("button", undefined, "jb-apply-button"); apply.type = "button";
131
+ apply.addEventListener("click", () => {
132
+ const weight = Number(strength.value);
133
+ const message = form.querySelector(".jb-message");
134
+ if (!strength.value.trim() || !Number.isFinite(weight) || weight < 0 || weight > 2) {
135
+ message.textContent = "強度を0~2の範囲で入力してください。"; strength.focus(); return;
136
+ }
137
+ if (!target || pending) return;
138
+ pending = true; targetOverride = true; updateTarget();
139
+ message.textContent = "";
140
+ props.value = {slots, selected:selected.name, slot:Number(target), strength:weight};
141
+ trigger("submit");
142
+ clearTimeout(pendingTimer);
143
+ pendingTimer = setTimeout(() => {
144
+ pending = false; updateTarget();
145
+ message.textContent = "応答を確認できませんでした。生成用LoRAの設定を確認してから再試行してください。";
146
+ }, 15000);
147
+ });
148
+ const message = make("p", "", "jb-message"); message.setAttribute("role", "status"); message.setAttribute("aria-live", "polite");
149
+ form.append(targetLabel, make("p", "", "jb-slot-summary"), strengthLabel, apply, message);
150
+ content.append(form); updateTarget();
151
+ }
152
+ function selectEntry(entry, card) {
153
+ if (pending) return;
154
+ selected = entry; returnCard = card; targetOverride = false;
155
+ grid.querySelectorAll(".jb-card").forEach(node => node.setAttribute("aria-pressed", String(node.dataset.name === entry.name)));
156
+ renderDetail();
157
+ if (media.matches) {
158
+ if (detail.open) detail.close();
159
+ oldOverflow = document.body.style.overflow; document.body.style.overflow = "hidden";
160
+ detail.showModal(); detail.scrollTop = 0;
161
+ } else if (!detail.open) detail.show();
162
+ }
163
+ function closeDetail() { detail.close(); }
164
+ detail.addEventListener("close", () => {
165
+ if (oldOverflow !== null) { document.body.style.overflow = oldOverflow; oldOverflow = null; }
166
+ if (media.matches) returnCard?.focus({preventScroll:true});
167
+ });
168
+ element.querySelector(".jb-close").addEventListener("click", closeDetail);
169
+ element.querySelector(".jb-zoom-close").addEventListener("click", () => zoom.close());
170
+ element.querySelector(".jb-prev").addEventListener("click", () => { page--; renderGrid(); });
171
+ element.querySelector(".jb-next").addEventListener("click", () => { page++; renderGrid(); });
172
+ category.addEventListener("change", () => { page = 0; renderGrid(); });
173
+ for (const option of category.options) {
174
+ const button = make("button", option.value); button.type = "button";
175
+ button.addEventListener("click", () => { category.value = option.value; page = 0; renderGrid(); });
176
+ element.querySelector(".jb-category-tabs").append(button);
177
+ }
178
+ search.addEventListener("input", () => { page = 0; renderGrid(); });
179
+ watch("value", () => {
180
+ if (props.value?.slots) slots = props.value.slots;
181
+ if (props.value?.message) {
182
+ clearTimeout(pendingTimer); pending = false;
183
+ const message = content.querySelector(".jb-message");
184
+ if (message) message.textContent = props.value.message;
185
+ }
186
+ updateTarget();
187
+ });
188
+ media.addEventListener("change", () => {
189
+ const first = page * pageSize; pageSize = media.matches ? 4 : 8; page = Math.floor(first / pageSize);
190
+ if (detail.open) detail.close();
191
+ if (!media.matches) detail.show();
192
+ renderGrid();
193
+ });
194
+ if (!media.matches) detail.show();
195
+ renderGrid();
assets/jujo/anima-LICENSE.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CircleStone Labs Non-Commercial License v1.2
2
+
3
+ CircleStone Labs LLC (“we” or “our” or “Company”) is pleased to make the weights, parameters, and inference code for the CircleStone Models (as defined below) freely available for your non-commercial and non-production use as set forth in this CircleStone Labs Non-Commercial License (“License”). “Models” includes all model elements, such as weights, algorithms, software, checkpoints, parameters, source code (inference code, evaluation code, and if applicable, fine-tuning code) and any other materials associated with the CircleStone AI models made available by Company under this License, including if any, the technical documentation, manuals, and instructions for the use and operation thereof (individually and collectively, the “CircleStone Models”). Note that we may also make available certain elements of what is included in the definition of “CircleStone Model” under a separate license, such as the inference code, and nothing in this License will be deemed to restrict or limit any other licenses granted by us in such elements.
4
+
5
+ By downloading, accessing, using, Distributing (as defined below), or creating a Derivative (as defined below) of the CircleStone Model, you agree to the terms of this License. If you do not agree to this License, then you do not have any rights to access, use, Distribute or create a Derivative of the CircleStone Model and you must immediately cease using the CircleStone Model. If you are agreeing to be bound by the terms of this License on behalf of your employer or other entity, you represent and warrant to us that you have full legal authority to bind your employer or such entity to this License. If you do not have the requisite authority, you may not accept the License or access the CircleStone Model on behalf of your employer or other entity.
6
+
7
+ 1. Definitions.
8
+ - a. “Derivative” means any (i) modified version of the CircleStone Model (including but not limited to any customized or fine-tuned version thereof), (ii) work based on the CircleStone Model, including Low-rank Adaptations (“LoRAs”) and textual inversions based on a CircleStone Model, or (iii) any other derivative work thereof. For the avoidance of doubt, Outputs are not considered Derivatives under this License.
9
+ - b. “Distribution” or “Distribute” or “Distributing” means providing or making available, by any means, a copy of the CircleStone Models and/or the Derivatives as the case may be, including by making the CircleStone Models and/or the Derivatives available to third-parties on a hosted basis.
10
+ - c. “Non-Commercial Purpose” means any of the following uses, but only so far as you do not receive any direct or indirect payment arising from the use of the CircleStone Model, or Derivatives: (i) personal use for research, experimentation, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, or otherwise not directly or indirectly connected to any commercial activities, business operations, or employment responsibilities; (ii) internal use by commercial or for-profit entities for testing, evaluation, or non-commercial research and development in a non-production environment; and (iii) use by any charitable organization for charitable purposes, or for testing or evaluation. For clarity, use (a) for revenue-generating activity, (b) in direct interactions with or that has impact on third-party end users, or (c) to train, fine tune, or distill other models for commercial use, in each case, is not a Non-Commercial Purpose.
11
+ - d. “Outputs” means any content generated by the operation of the CircleStone Models or Derivatives from an input (such as an image input) or prompt (i.e., text instructions) provided by users. For the avoidance of doubt, Outputs do not include any components of the CircleStone Models, such as any fine-tuned versions of the CircleStone Models, any LoRAs or textual inversions, the weights, or parameters.
12
+ - e. “you” or “your” means the individual or entity entering into this License with Company.
13
+
14
+ 2. License Grant.
15
+ - a. License. Subject to your compliance with this License, Company grants you a non-exclusive, worldwide, non-transferable, non-sublicensable, revocable, royalty free, and limited license to access, use, create Derivatives of, and Distribute the CircleStone Models and Derivatives solely for your Non-Commercial Purposes. The foregoing license is personal to you, and you may not assign or sublicense this License or any other rights or obligations under this License without Company’s prior written consent; any such assignment or sublicense will be void and will automatically and immediately terminate this License. Any restrictions set forth herein regarding the CircleStone Model also apply to any Derivative you create or that are created on your behalf.
16
+ - b. Non-Commercial Use Only. You may only access, use, Distribute, or create Derivatives of the CircleStone Model or Derivatives for Non-Commercial Purposes, unless otherwise expressly granted by this License. If you want to use a CircleStone Model or a Derivative for any purpose that is not expressly authorized under this License, such as for a commercial activity, you must request a license from Company, which Company may grant to you in Company’s sole discretion and which additional use may be subject to a fee, royalty or other revenue share. Please refer to the details in the Model Card if you would like a commercial license.
17
+ - c. Selling Derivatives. Persons operating in an individual capacity may sell Derivatives owned or created by them. This includes (i) requiring payment to download model weights, (ii) paid commissions for the creation of Derivatives, whether the resulting model is published publicly or delivered privately to the Commissioner, (iii) collecting donations or payments of any kind for the creation of a Derivative. This right to sell Derivatives extends solely to the model weights, and not to any larger product, tool, or feature which incorporates the Model.
18
+ - d. Reserved Rights. The grant of rights expressly set forth in this License are the complete grant of rights to you in the CircleStone Model, and no other licenses are granted, whether by waiver, estoppel, implication, equity, or otherwise except as otherwise agreed in writing by Company. Company and its licensors reserve all rights not expressly granted by this License.
19
+ - e. Outputs. We claim no ownership rights in and to the Outputs. You are solely responsible for the Outputs you generate and their subsequent uses in accordance with this License. You may use Outputs for any purpose (including for commercial purposes), except as expressly prohibited herein.
20
+ - f. You are solely responsible for your use of all Outputs generated through the Models and/or any Derivatives, including for ensuring that such Outputs are used in a manner that is non-infringing and otherwise compliant with all applicable laws, rules and regulations.
21
+
22
+ 3. Distribution. Subject to this License, you may Distribute copies of the CircleStone Model and/or Derivatives made by you, under the following conditions:
23
+ - a. you must make available a copy of this License to third-party recipients of the CircleStone Models and/or Derivatives you Distribute, and specify that any rights to use the CircleStone Models and/or Derivatives shall be directly granted by Company to said third-party recipients pursuant to this License;
24
+ - b. you must prominently display the following notice alongside the Distribution of the CircleStone Model or Derivative (such as via a “Notice” text file distributed as part of such CircleStone Model or Derivative) (the “Attribution Notice”):
25
+
26
+ “The CircleStone Model is licensed by CircleStone Labs LLC under the CircleStone Non-Commercial License. Copyright CircleStone Labs LLC.
27
+ IN NO EVENT SHALL CIRCLESTONE LABS LLC BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH USE OF THIS MODEL.”
28
+
29
+ - c. with respect to each licensee who purchases a commercial license to the Models, you agree that such licensee shall have the right to exercise such commercial license with respect to all Derivatives developed by you pursuant to the same terms and conditions agreed by the Company and such commercial licensee with respect to the Models; provided, that, you will not be entitled to receive any royalty fees, license fees or other remuneration in connection with the commercial licensee’s use of your Derivatives under such commercial license, and you hereby waive all right to receive any consideration in connection therewith;
30
+ - d. in the case of Distribution of Derivatives made by you: (i) you must also include in the Attribution Notice a statement that you have modified the applicable CircleStone Model; (ii) any terms and conditions you impose on any third-party recipients relating to Derivatives made by or for you shall neither limit such third-party recipients’ use of the CircleStone Model or any Derivatives made by or for Company in accordance with this License nor conflict with any of its terms and conditions and must include disclaimer of warranties and limitation of liability provisions that are at least as protective of Company as those set forth herein; and (iii) you must not misrepresent or imply, through any means, that the Derivatives made by or for you and/or any modified version of the CircleStone Model you Distribute under your name and responsibility is an official product of the Company or has been endorsed, approved or validated by the Company, unless you are authorized by Company to do so in writing; and
31
+
32
+ 4. Restrictions. You will not, and will not permit, assist or cause any third party to
33
+ - a. use, modify, copy, reproduce, create Derivatives of, or Distribute the CircleStone Model (or any Derivative thereof, or any data produced by the CircleStone Model), in whole or in part, (i) for any commercial or production purposes (with the exception that Outputs may be used commercially as per 2(d)), (ii) in any manner that infringes, misappropriates, or otherwise violates (or is likely to infringe, misappropriate, or otherwise violate) any third party’s legal rights, including rights of publicity or “digital replica” rights, (iii) in any unlawful, fraudulent, defamatory, or abusive activity, (iv) to generate unlawful content, including child sexual abuse material, or non-consensual intimate images; or (v) in any manner that violates any applicable laws, rules, regulations, directives, or governmental requirements;
34
+ - b. alter or remove copyright and other proprietary notices which appear on or in any portion of the CircleStone Model;
35
+ - c. utilize any equipment, device, software, or other means to circumvent or remove any security or protection used by Company in connection with the CircleStone Model, or to circumvent or remove any usage restrictions, or to enable functionality disabled by CircleStone Model;
36
+ - d. offer or impose any terms on the CircleStone Model that alter, restrict, or are inconsistent with the terms of this License; and
37
+ - e. directly or indirectly Distribute, export, or otherwise transfer CircleStone Model (i) to any individual, entity, or country prohibited by any applicable U.S. and non-U.S. export control and trade sanctions laws (“Export Laws”); (ii) to anyone on U.S. or non-U.S. government restricted parties lists; (iii) for any purpose prohibited by Export Laws, including nuclear, chemical or biological weapons, or missile technology applications; or (iv) use or download CircleStone Model if you or they are (a) located in a comprehensively sanctioned jurisdiction, (b) currently listed on any U.S. or non-U.S. restricted parties list, or (c) for any purpose prohibited by Export Laws.
38
+
39
+ 5. DISCLAIMERS. THE CIRCLESTONE MODELS ARE PROVIDED “AS IS” AND “WITH ALL FAULTS” WITH NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. COMPANY EXPRESSLY DISCLAIMS ALL REPRESENTATIONS AND WARRANTIES, EXPRESS OR IMPLIED, WHETHER BY STATUTE, CUSTOM, USAGE OR OTHERWISE AS TO ANY MATTERS RELATED TO THE CIRCLESTONE MODELS, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, SATISFACTORY QUALITY, OR NON-INFRINGEMENT. COMPANY MAKES NO WARRANTIES OR REPRESENTATIONS THAT THE CIRCLESTONE MODELS WILL BE ERROR FREE OR FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR PRODUCE ANY PARTICULAR RESULTS.
40
+
41
+ 6. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMITTED BY LAW, IN NO EVENT WILL COMPANY BE LIABLE TO YOU OR YOUR EMPLOYEES, AFFILIATES, USERS, OFFICERS OR DIRECTORS (A) UNDER ANY THEORY OF LIABILITY, WHETHER BASED IN CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY, WARRANTY, OR OTHERWISE UNDER THIS LICENSE, OR (B) FOR ANY INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, PUNITIVE OR SPECIAL DAMAGES OR LOST PROFITS, EVEN IF COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE CIRCLESTONE MODELS, THEIR CONSTITUENT COMPONENTS, AND ANY OUTPUT (COLLECTIVELY, “MODEL MATERIALS”) ARE NOT DESIGNED OR INTENDED FOR USE IN ANY APPLICATION OR SITUATION WHERE FAILURE OR FAULT OF THE MODEL MATERIALS COULD REASONABLY BE ANTICIPATED TO LEAD TO SERIOUS INJURY OF ANY PERSON, INCLUDING POTENTIAL DISCRIMINATION OR VIOLATION OF AN INDIVIDUAL’S PRIVACY RIGHTS, OR TO SEVERE PHYSICAL, PROPERTY, OR ENVIRONMENTAL DAMAGE (EACH, A “HIGH-RISK USE”). IF YOU ELECT TO USE ANY OF THE MODEL MATERIALS FOR A HIGH-RISK USE, YOU DO SO AT YOUR OWN RISK. YOU AGREE TO DESIGN AND IMPLEMENT APPROPRIATE DECISION-MAKING AND RISK-MITIGATION PROCEDURES AND POLICIES IN CONNECTION WITH A HIGH-RISK USE SUCH THAT EVEN IF THERE IS A FAILURE OR FAULT IN ANY OF THE MODEL MATERIALS, THE SAFETY OF PERSONS OR PROPERTY AFFECTED BY THE ACTIVITY STAYS AT A LEVEL THAT IS REASONABLE, APPROPRIATE, AND LAWFUL FOR THE FIELD OF THE HIGH-RISK USE.
42
+
43
+ 7. INDEMNIFICATION. You will indemnify, defend and hold harmless Company and our subsidiaries and affiliates, and each of our respective shareholders, directors, officers, employees, agents, successors, and assigns (collectively, the “Company Parties”) from and against any losses, liabilities, damages, fines, penalties, and expenses (including reasonable attorneys’ fees) incurred by any Company Party in connection with any claim, demand, allegation, lawsuit, proceeding, or investigation (collectively, “Claims”) arising out of or related to (a) your access to or use of the CircleStone Models (including in connection with any Output, results or data generated from such access or use), including any High-Risk Use; (b) your violation of this License; or (c) your violation, misappropriation or infringement of any rights of another (including intellectual property or other proprietary rights and privacy rights). You will promptly notify the Company Parties of any such Claims, and cooperate with Company Parties in defending such Claims. You will also grant the Company Parties sole control of the defense or settlement, at Company’s sole option, of any Claims. This indemnity is in addition to, and not in lieu of, any other indemnities or remedies set forth in a written agreement between you and Company or the other Company Parties.
44
+
45
+ 8. Termination; Survival.
46
+ - a. This License will automatically terminate upon any breach by you of the terms of this License.
47
+ - b. If you initiate any legal action or proceedings against Company or any other entity (including a cross-claim or counterclaim in a lawsuit), alleging that the CircleStone Models, any Derivative, or any part thereof, infringe upon intellectual property or other rights owned or licensable by you, then any licenses granted to you under this License will immediately terminate as of the date such legal action or claim is filed or initiated.
48
+ - c. Upon termination of this License, you must cease all use, access or Distribution of the CircleStone Model and any Derivatives. The following sections survive termination of this License: 2(c), 2(d), 4-11.
49
+
50
+ 9. Third Party Materials. The CircleStone Models may contain third-party software or other components (including free and open source software) (all of the foregoing, “Third Party Materials”), which are subject to the license terms of the respective third-party licensors. Your dealings or correspondence with third parties and your use of or interaction with any Third Party Materials are solely between you and the third party. Company does not control or endorse, and makes no representations or warranties regarding, any Third Party Materials, and your access to and use of such Third Party Materials are at your own risk.
51
+
52
+ 10. Trademarks. You have not been granted any trademark license as part of this License and may not use any name, logo or trademark associated with Company without the prior written permission of Company, except to the extent necessary to make the reference required in the Attribution Notice as specified above or as is reasonably necessary in describing the CircleStone Model and its creators.
53
+
54
+ 11. General. This License will be governed and construed under the laws of the State of Delaware without regard to conflicts of law provisions. If any provision or part of a provision of this License is unlawful, void or unenforceable, that provision or part of the provision is deemed severed from this License, and will not affect the validity and enforceability of any remaining provisions. The failure of Company to exercise or enforce any right or provision of this License will not operate as a waiver of such right or provision. This License does not confer any third-party beneficiary rights upon any other person or entity. This License, together with the documentation, contains the entire understanding between you and Company regarding the subject matter of this License, and supersedes all other written or oral agreements and understandings between you and Company regarding such subject matter. Notwithstanding the foregoing, if you enter into a commercial license with the Company, then such commercial license will supersede this License in all respects.
assets/jujo/anima-NOTICE.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NOTICE / 利用・帰属表示
2
+
3
+ ## English
4
+
5
+ This repository contains LoRAs derived from Anima by CircleStone Labs.
6
+
7
+ The CircleStone Model is licensed by CircleStone Labs LLC under the CircleStone Non-Commercial License. Copyright CircleStone Labs LLC. IN NO EVENT SHALL CIRCLESTONE LABS LLC BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH USE OF THIS MODEL.
8
+
9
+ The LoRAs distributed in this repository are modifications/derivatives of the applicable CircleStone Model. They are not official products of CircleStone Labs LLC and have not been endorsed, approved, or validated by CircleStone Labs LLC.
10
+
11
+ Use and redistribution of these LoRAs are subject to the applicable CircleStone Labs Non-Commercial License.
12
+
13
+ Official Anima license:
14
+ https://huggingface.co/circlestone-labs/Anima/blob/main/LICENSE.md
15
+
16
+ ---
17
+
18
+ ## 日本語
19
+
20
+ このリポジトリには、CircleStone Labs の Anima を基に作成した LoRA が含まれています。
21
+
22
+ CircleStone Model は、CircleStone Labs LLC により CircleStone Non-Commercial License の下でライセンスされています。Copyright CircleStone Labs LLC.
23
+ CircleStone Labs LLC は、本モデルの使用に起因または関連して生じるいかなる請求、損害、その他の責任についても、契約、不法行為、その他の法的根拠を問わず、一切の責任を負いません。
24
+
25
+ このリポジトリで配布される LoRA は、該当する CircleStone Model を変更して作成した派生物です。これらは CircleStone Labs LLC の公式製品ではなく、CircleStone Labs LLC による推奨、承認、または検証を受けたものではありません。
26
+
27
+ これらの LoRA の使用および再配布には、該当する CircleStone Labs Non-Commercial License が適用されます。
28
+
29
+ Anima 公式ライセンス:
30
+ https://huggingface.co/circlestone-labs/Anima/blob/main/LICENSE.md
31
+
32
+ ---
33
+
34
+ > **Note / 注記**
35
+ >
36
+ > The English attribution notice above includes the notice required by the upstream license. The Japanese text is provided for convenience. If there is any inconsistency, refer to the original English license text.
37
+ >
38
+ > 上記の英語部分には、上流ライセンスで要求される帰属表示が含まれています。日本語部分は理解を助けるための参考訳です。内容に相違がある場合は、原文の英語ライセンスを参照してください。
assets/jujo/anima.json ADDED
@@ -0,0 +1,791 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repo": "JujoHotaru/anima_lora",
3
+ "revision": "0c7fb6b7a7df1fefe0bf29a867362235f596a373",
4
+ "source": "https://huggingface.co/JujoHotaru/anima_lora",
5
+ "entries": [
6
+ {
7
+ "name": "doyagao_anima_v10.safetensors",
8
+ "path": "doyagao_anima_v10.safetensors",
9
+ "title": "V字形眉(ドヤ顔など)",
10
+ "doc": "doyagao/README.md",
11
+ "image": "doyagao/doyagao_anima_v10_1.webp",
12
+ "group_sample": false,
13
+ "size": 45974616
14
+ },
15
+ {
16
+ "name": "eye111A_anima_v10.safetensors",
17
+ "path": "eye111A_anima_v10.safetensors",
18
+ "title": "いろいろな眼の形",
19
+ "doc": "eyeshapes/README.md",
20
+ "image": "eyeshapes/samples_v10/eye111A_anima_v10.webp",
21
+ "group_sample": false,
22
+ "size": 11566424
23
+ },
24
+ {
25
+ "name": "eye116A_anima_v10.safetensors",
26
+ "path": "eye116A_anima_v10.safetensors",
27
+ "title": "いろいろな眼の形",
28
+ "doc": "eyeshapes/README.md",
29
+ "image": "eyeshapes/samples_v10/eye116A_anima_v10.webp",
30
+ "group_sample": false,
31
+ "size": 11566424
32
+ },
33
+ {
34
+ "name": "eye118A_anima_v10.safetensors",
35
+ "path": "eye118A_anima_v10.safetensors",
36
+ "title": "いろいろな眼の形",
37
+ "doc": "eyeshapes/README.md",
38
+ "image": "eyeshapes/samples_v10/eye118A_anima_v10.webp",
39
+ "group_sample": false,
40
+ "size": 11566424
41
+ },
42
+ {
43
+ "name": "eye124A_anima_v10.safetensors",
44
+ "path": "eye124A_anima_v10.safetensors",
45
+ "title": "いろいろな眼の形",
46
+ "doc": "eyeshapes/README.md",
47
+ "image": "eyeshapes/samples_v10/eye124A_anima_v10.webp",
48
+ "group_sample": false,
49
+ "size": 11566424
50
+ },
51
+ {
52
+ "name": "eye124B_anima_v10.safetensors",
53
+ "path": "eye124B_anima_v10.safetensors",
54
+ "title": "いろいろな眼の形",
55
+ "doc": "eyeshapes/README.md",
56
+ "image": "eyeshapes/samples_v10/eye124B_anima_v10.webp",
57
+ "group_sample": false,
58
+ "size": 11566424
59
+ },
60
+ {
61
+ "name": "eye125A_anima_v10.safetensors",
62
+ "path": "eye125A_anima_v10.safetensors",
63
+ "title": "いろいろな眼の形",
64
+ "doc": "eyeshapes/README.md",
65
+ "image": "eyeshapes/samples_v10/eye125A_anima_v10.webp",
66
+ "group_sample": false,
67
+ "size": 11566424
68
+ },
69
+ {
70
+ "name": "eye126A_anima_v10.safetensors",
71
+ "path": "eye126A_anima_v10.safetensors",
72
+ "title": "いろいろな眼の形",
73
+ "doc": "eyeshapes/README.md",
74
+ "image": "eyeshapes/samples_v10/eye126A_anima_v10.webp",
75
+ "group_sample": false,
76
+ "size": 11566424
77
+ },
78
+ {
79
+ "name": "eye127A_anima_v10.safetensors",
80
+ "path": "eye127A_anima_v10.safetensors",
81
+ "title": "いろいろな眼の形",
82
+ "doc": "eyeshapes/README.md",
83
+ "image": "eyeshapes/samples_v10/eye127A_anima_v10.webp",
84
+ "group_sample": false,
85
+ "size": 11566424
86
+ },
87
+ {
88
+ "name": "eye130A_anima_v10.safetensors",
89
+ "path": "eye130A_anima_v10.safetensors",
90
+ "title": "いろいろな眼の形",
91
+ "doc": "eyeshapes/README.md",
92
+ "image": "eyeshapes/samples_v10/eye130A_anima_v10.webp",
93
+ "group_sample": false,
94
+ "size": 11566424
95
+ },
96
+ {
97
+ "name": "eye136A_anima_v10.safetensors",
98
+ "path": "eye136A_anima_v10.safetensors",
99
+ "title": "いろいろな眼の形",
100
+ "doc": "eyeshapes/README.md",
101
+ "image": "eyeshapes/samples_v10/eye136A_anima_v10.webp",
102
+ "group_sample": false,
103
+ "size": 11566424
104
+ },
105
+ {
106
+ "name": "eye144A_anima_v10.safetensors",
107
+ "path": "eye144A_anima_v10.safetensors",
108
+ "title": "いろいろな眼の形",
109
+ "doc": "eyeshapes/README.md",
110
+ "image": "eyeshapes/samples_v10/eye144A_anima_v10.webp",
111
+ "group_sample": false,
112
+ "size": 11566424
113
+ },
114
+ {
115
+ "name": "eye147A_anima_v10.safetensors",
116
+ "path": "eye147A_anima_v10.safetensors",
117
+ "title": "いろいろな眼の形",
118
+ "doc": "eyeshapes/README.md",
119
+ "image": "eyeshapes/samples_v10/eye147A_anima_v10.webp",
120
+ "group_sample": false,
121
+ "size": 11566424
122
+ },
123
+ {
124
+ "name": "eye152A_anima_v10.safetensors",
125
+ "path": "eye152A_anima_v10.safetensors",
126
+ "title": "いろいろな眼の形",
127
+ "doc": "eyeshapes/README.md",
128
+ "image": "eyeshapes/samples_v10/eye152A_anima_v10.webp",
129
+ "group_sample": false,
130
+ "size": 11566424
131
+ },
132
+ {
133
+ "name": "eye157A_anima_v10.safetensors",
134
+ "path": "eye157A_anima_v10.safetensors",
135
+ "title": "いろいろな眼の形",
136
+ "doc": "eyeshapes/README.md",
137
+ "image": "eyeshapes/samples_v10/eye157A_anima_v10.webp",
138
+ "group_sample": false,
139
+ "size": 11566424
140
+ },
141
+ {
142
+ "name": "eye159A_anima_v10.safetensors",
143
+ "path": "eye159A_anima_v10.safetensors",
144
+ "title": "いろいろな眼の形",
145
+ "doc": "eyeshapes/README.md",
146
+ "image": "eyeshapes/samples_v10/eye159A_anima_v10.webp",
147
+ "group_sample": false,
148
+ "size": 11566424
149
+ },
150
+ {
151
+ "name": "eye160A_anima_v10.safetensors",
152
+ "path": "eye160A_anima_v10.safetensors",
153
+ "title": "いろいろな眼の形",
154
+ "doc": "eyeshapes/README.md",
155
+ "image": "eyeshapes/samples_v10/eye160A_anima_v10.webp",
156
+ "group_sample": false,
157
+ "size": 11566424
158
+ },
159
+ {
160
+ "name": "eye163A_anima_v10.safetensors",
161
+ "path": "eye163A_anima_v10.safetensors",
162
+ "title": "いろいろな眼の形",
163
+ "doc": "eyeshapes/README.md",
164
+ "image": "eyeshapes/samples_v10/eye163A_anima_v10.webp",
165
+ "group_sample": false,
166
+ "size": 11566424
167
+ },
168
+ {
169
+ "name": "eye178A_anima_v10.safetensors",
170
+ "path": "eye178A_anima_v10.safetensors",
171
+ "title": "いろいろな眼の形",
172
+ "doc": "eyeshapes/README.md",
173
+ "image": "eyeshapes/samples_v10/eye178A_anima_v10.webp",
174
+ "group_sample": false,
175
+ "size": 11566424
176
+ },
177
+ {
178
+ "name": "eye180A_anima_v10.safetensors",
179
+ "path": "eye180A_anima_v10.safetensors",
180
+ "title": "いろいろな眼の形",
181
+ "doc": "eyeshapes/README.md",
182
+ "image": "eyeshapes/samples_v10/eye180A_anima_v10.webp",
183
+ "group_sample": false,
184
+ "size": 11566424
185
+ },
186
+ {
187
+ "name": "eye188A_anima_v10.safetensors",
188
+ "path": "eye188A_anima_v10.safetensors",
189
+ "title": "いろいろな眼の形",
190
+ "doc": "eyeshapes/README.md",
191
+ "image": "eyeshapes/samples_v10/eye188A_anima_v10.webp",
192
+ "group_sample": false,
193
+ "size": 11566424
194
+ },
195
+ {
196
+ "name": "eye196A_anima_v10.safetensors",
197
+ "path": "eye196A_anima_v10.safetensors",
198
+ "title": "いろいろな眼の形",
199
+ "doc": "eyeshapes/README.md",
200
+ "image": "eyeshapes/samples_v10/eye196A_anima_v10.webp",
201
+ "group_sample": false,
202
+ "size": 11566424
203
+ },
204
+ {
205
+ "name": "eye201A_anima_v10.safetensors",
206
+ "path": "eye201A_anima_v10.safetensors",
207
+ "title": "いろいろな眼の形",
208
+ "doc": "eyeshapes/README.md",
209
+ "image": "eyeshapes/samples_v10/eye201A_anima_v10.webp",
210
+ "group_sample": false,
211
+ "size": 11566424
212
+ },
213
+ {
214
+ "name": "eye202A_anima_v10.safetensors",
215
+ "path": "eye202A_anima_v10.safetensors",
216
+ "title": "いろいろな眼の形",
217
+ "doc": "eyeshapes/README.md",
218
+ "image": "eyeshapes/samples_v10/eye202A_anima_v10.webp",
219
+ "group_sample": false,
220
+ "size": 11566424
221
+ },
222
+ {
223
+ "name": "eye202B_anima_v10.safetensors",
224
+ "path": "eye202B_anima_v10.safetensors",
225
+ "title": "いろいろな眼の形",
226
+ "doc": "eyeshapes/README.md",
227
+ "image": "eyeshapes/samples_v10/eye202B_anima_v10.webp",
228
+ "group_sample": false,
229
+ "size": 11566424
230
+ },
231
+ {
232
+ "name": "eye202C_anima_v10.safetensors",
233
+ "path": "eye202C_anima_v10.safetensors",
234
+ "title": "いろいろな眼の形",
235
+ "doc": "eyeshapes/README.md",
236
+ "image": "eyeshapes/samples_v10/eye202C_anima_v10.webp",
237
+ "group_sample": false,
238
+ "size": 11566424
239
+ },
240
+ {
241
+ "name": "eye203A_anima_v10.safetensors",
242
+ "path": "eye203A_anima_v10.safetensors",
243
+ "title": "いろいろな眼の形",
244
+ "doc": "eyeshapes/README.md",
245
+ "image": "eyeshapes/samples_v10/eye203A_anima_v10.webp",
246
+ "group_sample": false,
247
+ "size": 11566424
248
+ },
249
+ {
250
+ "name": "eye206A_anima_v10.safetensors",
251
+ "path": "eye206A_anima_v10.safetensors",
252
+ "title": "いろいろな眼の形",
253
+ "doc": "eyeshapes/README.md",
254
+ "image": "eyeshapes/samples_v10/eye206A_anima_v10.webp",
255
+ "group_sample": false,
256
+ "size": 11566424
257
+ },
258
+ {
259
+ "name": "eye229A_anima_v10.safetensors",
260
+ "path": "eye229A_anima_v10.safetensors",
261
+ "title": "いろいろな眼の形",
262
+ "doc": "eyeshapes/README.md",
263
+ "image": "eyeshapes/samples_v10/eye229A_anima_v10.webp",
264
+ "group_sample": false,
265
+ "size": 11566424
266
+ },
267
+ {
268
+ "name": "eye237A_anima_v10.safetensors",
269
+ "path": "eye237A_anima_v10.safetensors",
270
+ "title": "いろいろな眼の形",
271
+ "doc": "eyeshapes/README.md",
272
+ "image": "eyeshapes/samples_v10/eye237A_anima_v10.webp",
273
+ "group_sample": false,
274
+ "size": 11566424
275
+ },
276
+ {
277
+ "name": "eye238A_anima_v10.safetensors",
278
+ "path": "eye238A_anima_v10.safetensors",
279
+ "title": "いろいろな眼の形",
280
+ "doc": "eyeshapes/README.md",
281
+ "image": "eyeshapes/samples_v10/eye238A_anima_v10.webp",
282
+ "group_sample": false,
283
+ "size": 11566424
284
+ },
285
+ {
286
+ "name": "eye242A_anima_v10.safetensors",
287
+ "path": "eye242A_anima_v10.safetensors",
288
+ "title": "いろいろな眼の形",
289
+ "doc": "eyeshapes/README.md",
290
+ "image": "eyeshapes/samples_v10/eye242A_anima_v10.webp",
291
+ "group_sample": false,
292
+ "size": 11566424
293
+ },
294
+ {
295
+ "name": "eye248A_anima_v10.safetensors",
296
+ "path": "eye248A_anima_v10.safetensors",
297
+ "title": "いろいろな眼の形",
298
+ "doc": "eyeshapes/README.md",
299
+ "image": "eyeshapes/samples_v10/eye248A_anima_v10.webp",
300
+ "group_sample": false,
301
+ "size": 11566424
302
+ },
303
+ {
304
+ "name": "eye254A_anima_v10.safetensors",
305
+ "path": "eye254A_anima_v10.safetensors",
306
+ "title": "いろいろな眼の形",
307
+ "doc": "eyeshapes/README.md",
308
+ "image": "eyeshapes/samples_v10/eye254A_anima_v10.webp",
309
+ "group_sample": false,
310
+ "size": 11566424
311
+ },
312
+ {
313
+ "name": "eye255A_anima_v10.safetensors",
314
+ "path": "eye255A_anima_v10.safetensors",
315
+ "title": "いろいろな眼の形",
316
+ "doc": "eyeshapes/README.md",
317
+ "image": "eyeshapes/samples_v10/eye255A_anima_v10.webp",
318
+ "group_sample": false,
319
+ "size": 11566424
320
+ },
321
+ {
322
+ "name": "eye258A_anima_v10.safetensors",
323
+ "path": "eye258A_anima_v10.safetensors",
324
+ "title": "いろいろな眼の形",
325
+ "doc": "eyeshapes/README.md",
326
+ "image": "eyeshapes/samples_v10/eye258A_anima_v10.webp",
327
+ "group_sample": false,
328
+ "size": 11566424
329
+ },
330
+ {
331
+ "name": "eye274A_anima_v10.safetensors",
332
+ "path": "eye274A_anima_v10.safetensors",
333
+ "title": "いろいろな眼の形",
334
+ "doc": "eyeshapes/README.md",
335
+ "image": "eyeshapes/samples_v10/eye274A_anima_v10.webp",
336
+ "group_sample": false,
337
+ "size": 11566424
338
+ },
339
+ {
340
+ "name": "eye277A_anima_v10.safetensors",
341
+ "path": "eye277A_anima_v10.safetensors",
342
+ "title": "いろいろな眼の形",
343
+ "doc": "eyeshapes/README.md",
344
+ "image": "eyeshapes/samples_v10/eye277A_anima_v10.webp",
345
+ "group_sample": false,
346
+ "size": 11566424
347
+ },
348
+ {
349
+ "name": "eye281A_anima_v10.safetensors",
350
+ "path": "eye281A_anima_v10.safetensors",
351
+ "title": "いろいろな眼の形",
352
+ "doc": "eyeshapes/README.md",
353
+ "image": "eyeshapes/samples_v10/eye281A_anima_v10.webp",
354
+ "group_sample": false,
355
+ "size": 11566424
356
+ },
357
+ {
358
+ "name": "eye284A_anima_v10.safetensors",
359
+ "path": "eye284A_anima_v10.safetensors",
360
+ "title": "いろいろな眼の形",
361
+ "doc": "eyeshapes/README.md",
362
+ "image": "eyeshapes/samples_v10/eye284A_anima_v10.webp",
363
+ "group_sample": false,
364
+ "size": 11566424
365
+ },
366
+ {
367
+ "name": "eye287A_anima_v10.safetensors",
368
+ "path": "eye287A_anima_v10.safetensors",
369
+ "title": "いろいろな眼の形",
370
+ "doc": "eyeshapes/README.md",
371
+ "image": "eyeshapes/samples_v10/eye287A_anima_v10.webp",
372
+ "group_sample": false,
373
+ "size": 11566424
374
+ },
375
+ {
376
+ "name": "eye288A_anima_v10.safetensors",
377
+ "path": "eye288A_anima_v10.safetensors",
378
+ "title": "いろいろな眼の形",
379
+ "doc": "eyeshapes/README.md",
380
+ "image": "eyeshapes/samples_v10/eye288A_anima_v10.webp",
381
+ "group_sample": false,
382
+ "size": 11566424
383
+ },
384
+ {
385
+ "name": "eye289A_anima_v10.safetensors",
386
+ "path": "eye289A_anima_v10.safetensors",
387
+ "title": "いろいろな眼の形",
388
+ "doc": "eyeshapes/README.md",
389
+ "image": "eyeshapes/samples_v10/eye289A_anima_v10.webp",
390
+ "group_sample": false,
391
+ "size": 11566424
392
+ },
393
+ {
394
+ "name": "eye290A_anima_v10.safetensors",
395
+ "path": "eye290A_anima_v10.safetensors",
396
+ "title": "いろいろな眼の形",
397
+ "doc": "eyeshapes/README.md",
398
+ "image": "eyeshapes/samples_v10/eye290A_anima_v10.webp",
399
+ "group_sample": false,
400
+ "size": 11566424
401
+ },
402
+ {
403
+ "name": "eye291A_anima_v10.safetensors",
404
+ "path": "eye291A_anima_v10.safetensors",
405
+ "title": "いろいろな眼の形",
406
+ "doc": "eyeshapes/README.md",
407
+ "image": "eyeshapes/samples_v10/eye291A_anima_v10.webp",
408
+ "group_sample": false,
409
+ "size": 11566424
410
+ },
411
+ {
412
+ "name": "eye292A_anima_v10.safetensors",
413
+ "path": "eye292A_anima_v10.safetensors",
414
+ "title": "いろいろな眼の形",
415
+ "doc": "eyeshapes/README.md",
416
+ "image": "eyeshapes/samples_v10/eye292A_anima_v10.webp",
417
+ "group_sample": false,
418
+ "size": 11566424
419
+ },
420
+ {
421
+ "name": "eye293A_anima_v10.safetensors",
422
+ "path": "eye293A_anima_v10.safetensors",
423
+ "title": "いろいろな眼の形",
424
+ "doc": "eyeshapes/README.md",
425
+ "image": "eyeshapes/samples_v10/eye293A_anima_v10.webp",
426
+ "group_sample": false,
427
+ "size": 11566424
428
+ },
429
+ {
430
+ "name": "eye303A_anima_v10.safetensors",
431
+ "path": "eye303A_anima_v10.safetensors",
432
+ "title": "いろいろな眼の形",
433
+ "doc": "eyeshapes/README.md",
434
+ "image": "eyeshapes/samples_v10/eye303A_anima_v10.webp",
435
+ "group_sample": false,
436
+ "size": 11566424
437
+ },
438
+ {
439
+ "name": "eye305A_anima_v10.safetensors",
440
+ "path": "eye305A_anima_v10.safetensors",
441
+ "title": "いろいろな眼の形",
442
+ "doc": "eyeshapes/README.md",
443
+ "image": "eyeshapes/samples_v10/eye305A_anima_v10.webp",
444
+ "group_sample": false,
445
+ "size": 11566424
446
+ },
447
+ {
448
+ "name": "eye306A_anima_v10.safetensors",
449
+ "path": "eye306A_anima_v10.safetensors",
450
+ "title": "いろいろな眼の形",
451
+ "doc": "eyeshapes/README.md",
452
+ "image": "eyeshapes/samples_v10/eye306A_anima_v10.webp",
453
+ "group_sample": false,
454
+ "size": 11566424
455
+ },
456
+ {
457
+ "name": "eye313A_anima_v10.safetensors",
458
+ "path": "eye313A_anima_v10.safetensors",
459
+ "title": "いろいろな眼の形",
460
+ "doc": "eyeshapes/README.md",
461
+ "image": "eyeshapes/samples_v10/eye313A_anima_v10.webp",
462
+ "group_sample": false,
463
+ "size": 11566424
464
+ },
465
+ {
466
+ "name": "eye314A_anima_v10.safetensors",
467
+ "path": "eye314A_anima_v10.safetensors",
468
+ "title": "いろいろな眼の形",
469
+ "doc": "eyeshapes/README.md",
470
+ "image": "eyeshapes/samples_v10/eye314A_anima_v10.webp",
471
+ "group_sample": false,
472
+ "size": 11566424
473
+ },
474
+ {
475
+ "name": "eye315A_anima_v10.safetensors",
476
+ "path": "eye315A_anima_v10.safetensors",
477
+ "title": "いろいろな眼の形",
478
+ "doc": "eyeshapes/README.md",
479
+ "image": "eyeshapes/samples_v10/eye315A_anima_v10.webp",
480
+ "group_sample": false,
481
+ "size": 11566424
482
+ },
483
+ {
484
+ "name": "eye316A_anima_v10.safetensors",
485
+ "path": "eye316A_anima_v10.safetensors",
486
+ "title": "いろいろな眼の形",
487
+ "doc": "eyeshapes/README.md",
488
+ "image": "eyeshapes/samples_v10/eye316A_anima_v10.webp",
489
+ "group_sample": false,
490
+ "size": 11566424
491
+ },
492
+ {
493
+ "name": "eye704A_anima_v10.safetensors",
494
+ "path": "eye704A_anima_v10.safetensors",
495
+ "title": "いろいろな眼の形",
496
+ "doc": "eyeshapes/README.md",
497
+ "image": "eyeshapes/samples_v10/eye704A_anima_v10.webp",
498
+ "group_sample": false,
499
+ "size": 11566424
500
+ },
501
+ {
502
+ "name": "eye706A_anima_v10.safetensors",
503
+ "path": "eye706A_anima_v10.safetensors",
504
+ "title": "いろいろな眼の形",
505
+ "doc": "eyeshapes/README.md",
506
+ "image": "eyeshapes/samples_v10/eye706A_anima_v10.webp",
507
+ "group_sample": false,
508
+ "size": 11566424
509
+ },
510
+ {
511
+ "name": "eye707A_anima_v10.safetensors",
512
+ "path": "eye707A_anima_v10.safetensors",
513
+ "title": "いろいろな眼の形",
514
+ "doc": "eyeshapes/README.md",
515
+ "image": "eyeshapes/samples_v10/eye707A_anima_v10.webp",
516
+ "group_sample": false,
517
+ "size": 11566424
518
+ },
519
+ {
520
+ "name": "eye708A_anima_v10.safetensors",
521
+ "path": "eye708A_anima_v10.safetensors",
522
+ "title": "いろいろな眼の形",
523
+ "doc": "eyeshapes/README.md",
524
+ "image": "eyeshapes/samples_v10/eye708A_anima_v10.webp",
525
+ "group_sample": false,
526
+ "size": 11566424
527
+ },
528
+ {
529
+ "name": "eye709A_anima_v10.safetensors",
530
+ "path": "eye709A_anima_v10.safetensors",
531
+ "title": "いろいろな眼の形",
532
+ "doc": "eyeshapes/README.md",
533
+ "image": "eyeshapes/samples_v10/eye709A_anima_v10.webp",
534
+ "group_sample": false,
535
+ "size": 11566424
536
+ },
537
+ {
538
+ "name": "eye710A_anima_v10.safetensors",
539
+ "path": "eye710A_anima_v10.safetensors",
540
+ "title": "いろいろな眼の形",
541
+ "doc": "eyeshapes/README.md",
542
+ "image": "eyeshapes/samples_v10/eye710A_anima_v10.webp",
543
+ "group_sample": false,
544
+ "size": 11566424
545
+ },
546
+ {
547
+ "name": "eye715A_anima_v10.safetensors",
548
+ "path": "eye715A_anima_v10.safetensors",
549
+ "title": "いろいろな眼の形",
550
+ "doc": "eyeshapes/README.md",
551
+ "image": "eyeshapes/samples_v10/eye715A_anima_v10.webp",
552
+ "group_sample": false,
553
+ "size": 11566424
554
+ },
555
+ {
556
+ "name": "eye716A_anima_v10.safetensors",
557
+ "path": "eye716A_anima_v10.safetensors",
558
+ "title": "いろいろな眼の形",
559
+ "doc": "eyeshapes/README.md",
560
+ "image": "eyeshapes/samples_v10/eye716A_anima_v10.webp",
561
+ "group_sample": false,
562
+ "size": 11566424
563
+ },
564
+ {
565
+ "name": "eye718A_anima_v10.safetensors",
566
+ "path": "eye718A_anima_v10.safetensors",
567
+ "title": "いろいろな眼の形",
568
+ "doc": "eyeshapes/README.md",
569
+ "image": "eyeshapes/samples_v10/eye718A_anima_v10.webp",
570
+ "group_sample": false,
571
+ "size": 11566424
572
+ },
573
+ {
574
+ "name": "eye719A_anima_v10.safetensors",
575
+ "path": "eye719A_anima_v10.safetensors",
576
+ "title": "いろいろな眼の形",
577
+ "doc": "eyeshapes/README.md",
578
+ "image": "eyeshapes/samples_v10/eye719A_anima_v10.webp",
579
+ "group_sample": false,
580
+ "size": 11566424
581
+ },
582
+ {
583
+ "name": "eye721A_anima_v10.safetensors",
584
+ "path": "eye721A_anima_v10.safetensors",
585
+ "title": "いろいろな眼の形",
586
+ "doc": "eyeshapes/README.md",
587
+ "image": "eyeshapes/samples_v10/eye721A_anima_v10.webp",
588
+ "group_sample": false,
589
+ "size": 11566424
590
+ },
591
+ {
592
+ "name": "eye727A_anima_v10.safetensors",
593
+ "path": "eye727A_anima_v10.safetensors",
594
+ "title": "いろいろな眼の形",
595
+ "doc": "eyeshapes/README.md",
596
+ "image": "eyeshapes/samples_v10/eye727A_anima_v10.webp",
597
+ "group_sample": false,
598
+ "size": 11566424
599
+ },
600
+ {
601
+ "name": "eye729A_anima_v10.safetensors",
602
+ "path": "eye729A_anima_v10.safetensors",
603
+ "title": "いろいろな眼の形",
604
+ "doc": "eyeshapes/README.md",
605
+ "image": "eyeshapes/samples_v10/eye729A_anima_v10.webp",
606
+ "group_sample": false,
607
+ "size": 11566424
608
+ },
609
+ {
610
+ "name": "eye732A_anima_v10.safetensors",
611
+ "path": "eye732A_anima_v10.safetensors",
612
+ "title": "いろいろな眼の形",
613
+ "doc": "eyeshapes/README.md",
614
+ "image": "eyeshapes/samples_v10/eye732A_anima_v10.webp",
615
+ "group_sample": false,
616
+ "size": 11566424
617
+ },
618
+ {
619
+ "name": "eye734A_anima_v10.safetensors",
620
+ "path": "eye734A_anima_v10.safetensors",
621
+ "title": "いろいろな眼の形",
622
+ "doc": "eyeshapes/README.md",
623
+ "image": "eyeshapes/samples_v10/eye734A_anima_v10.webp",
624
+ "group_sample": false,
625
+ "size": 11566424
626
+ },
627
+ {
628
+ "name": "eye736A_anima_v10.safetensors",
629
+ "path": "eye736A_anima_v10.safetensors",
630
+ "title": "いろいろな眼の形",
631
+ "doc": "eyeshapes/README.md",
632
+ "image": "eyeshapes/samples_v10/eye736A_anima_v10.webp",
633
+ "group_sample": false,
634
+ "size": 11566424
635
+ },
636
+ {
637
+ "name": "eye737A_anima_v10.safetensors",
638
+ "path": "eye737A_anima_v10.safetensors",
639
+ "title": "いろいろな眼の形",
640
+ "doc": "eyeshapes/README.md",
641
+ "image": "eyeshapes/samples_v10/eye737A_anima_v10.webp",
642
+ "group_sample": false,
643
+ "size": 11566424
644
+ },
645
+ {
646
+ "name": "eye740A_anima_v10.safetensors",
647
+ "path": "eye740A_anima_v10.safetensors",
648
+ "title": "いろいろな眼の形",
649
+ "doc": "eyeshapes/README.md",
650
+ "image": "eyeshapes/samples_v10/eye740A_anima_v10.webp",
651
+ "group_sample": false,
652
+ "size": 45974616
653
+ },
654
+ {
655
+ "name": "eye740B_anima_v10.safetensors",
656
+ "path": "eye740B_anima_v10.safetensors",
657
+ "title": "いろいろな眼の形",
658
+ "doc": "eyeshapes/README.md",
659
+ "image": "eyeshapes/samples_v10/eye740B_anima_v10.webp",
660
+ "group_sample": false,
661
+ "size": 45974616
662
+ },
663
+ {
664
+ "name": "eye741A_anima_v10.safetensors",
665
+ "path": "eye741A_anima_v10.safetensors",
666
+ "title": "いろいろな眼の形",
667
+ "doc": "eyeshapes/README.md",
668
+ "image": "eyeshapes/samples_v10/eye741A_anima_v10.webp",
669
+ "group_sample": false,
670
+ "size": 11566424
671
+ },
672
+ {
673
+ "name": "eye742A_anima_v10.safetensors",
674
+ "path": "eye742A_anima_v10.safetensors",
675
+ "title": "いろいろな眼の形",
676
+ "doc": "eyeshapes/README.md",
677
+ "image": "eyeshapes/samples_v10/eye742A_anima_v10.webp",
678
+ "group_sample": false,
679
+ "size": 11566424
680
+ },
681
+ {
682
+ "name": "inv_v_eyebrows_anima_v10.safetensors",
683
+ "path": "inv_v_eyebrows_anima_v10.safetensors",
684
+ "title": "ハの字型の眉",
685
+ "doc": "inv_v_eyebrows/README.md",
686
+ "image": "inv_v_eyebrows/inv_v_eyebrows_anima_v10_1.webp",
687
+ "group_sample": false,
688
+ "size": 11566424
689
+ },
690
+ {
691
+ "name": "erase_human_ear_anima_v10.safetensors",
692
+ "path": "erase_human_ear_anima_v10.safetensors",
693
+ "title": "人耳消去(ケモ耳用)",
694
+ "doc": "human_ear_eraser/README.md",
695
+ "image": "human_ear_eraser/thumb.webp",
696
+ "group_sample": true,
697
+ "size": 45974616
698
+ },
699
+ {
700
+ "name": "widemouth1_anima_v10.safetensors",
701
+ "path": "widemouth1_anima_v10.safetensors",
702
+ "title": "大きな口 (Wide mouth)",
703
+ "doc": "widemouth/README.md",
704
+ "image": "widemouth/widemouth1_anima_v10.webp",
705
+ "group_sample": false,
706
+ "size": 11566424
707
+ },
708
+ {
709
+ "name": "widemouth2_anima_v10.safetensors",
710
+ "path": "widemouth2_anima_v10.safetensors",
711
+ "title": "大きな口 (Wide mouth)",
712
+ "doc": "widemouth/README.md",
713
+ "image": "widemouth/widemouth2_anima_v10.webp",
714
+ "group_sample": false,
715
+ "size": 11567264
716
+ },
717
+ {
718
+ "name": "widemouth3_anima_v10.safetensors",
719
+ "path": "widemouth3_anima_v10.safetensors",
720
+ "title": "大きな口 (Wide mouth)",
721
+ "doc": "widemouth/README.md",
722
+ "image": "widemouth/widemouth3_anima_v10.webp",
723
+ "group_sample": false,
724
+ "size": 11567264
725
+ },
726
+ {
727
+ "name": "widemouth4_anima_v10.safetensors",
728
+ "path": "widemouth4_anima_v10.safetensors",
729
+ "title": "大きな口 (Wide mouth)",
730
+ "doc": "widemouth/README.md",
731
+ "image": "widemouth/widemouth4_anima_v10.webp",
732
+ "group_sample": false,
733
+ "size": 11567264
734
+ },
735
+ {
736
+ "name": "smallclosedmouth1_anima_v10.safetensors",
737
+ "path": "smallclosedmouth1_anima_v10.safetensors",
738
+ "title": "小さな口 (Small mouth)",
739
+ "doc": "smallmouth/README.md",
740
+ "image": "smallmouth/smallclosedmouth1_anima_v10.webp",
741
+ "group_sample": false,
742
+ "size": 11567264
743
+ },
744
+ {
745
+ "name": "smallclosedmouth2_anima_v10.safetensors",
746
+ "path": "smallclosedmouth2_anima_v10.safetensors",
747
+ "title": "小さな口 (Small mouth)",
748
+ "doc": "smallmouth/README.md",
749
+ "image": "smallmouth/smallclosedmouth2_anima_v10.webp",
750
+ "group_sample": false,
751
+ "size": 11566424
752
+ },
753
+ {
754
+ "name": "smallclosedmouth3_anima_v10.safetensors",
755
+ "path": "smallclosedmouth3_anima_v10.safetensors",
756
+ "title": "小さな口 (Small mouth)",
757
+ "doc": "smallmouth/README.md",
758
+ "image": "smallmouth/smallclosedmouth3_anima_v10.webp",
759
+ "group_sample": false,
760
+ "size": 11566424
761
+ },
762
+ {
763
+ "name": "smallmouth1_anima_v10.safetensors",
764
+ "path": "smallmouth1_anima_v10.safetensors",
765
+ "title": "小さな口 (Small mouth)",
766
+ "doc": "smallmouth/README.md",
767
+ "image": "smallmouth/smallmouth1_anima_v10.webp",
768
+ "group_sample": false,
769
+ "size": 11566424
770
+ },
771
+ {
772
+ "name": "darkness_anima_v10.safetensors",
773
+ "path": "darkness_anima_v10.safetensors",
774
+ "title": "暗闇化",
775
+ "doc": "darkness/README.md",
776
+ "image": "darkness/thumb.webp",
777
+ "group_sample": true,
778
+ "size": 11566424
779
+ },
780
+ {
781
+ "name": "paleface_anima_v10.safetensors",
782
+ "path": "paleface_anima_v10.safetensors",
783
+ "title": "青ざめた顔",
784
+ "doc": "paleface/README.md",
785
+ "image": "paleface/thumb.webp",
786
+ "group_sample": true,
787
+ "size": 11566424
788
+ }
789
+ ],
790
+ "excluded_pony": []
791
+ }
assets/jujo/illustrious-LICENSE ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Copyright 2023 Hotaru Jujo
2
+ https://huggingface.co/JujoHotaru
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
assets/jujo/illustrious.json ADDED
@@ -0,0 +1,1783 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repo": "JujoHotaru/lora",
3
+ "revision": "9e51c3d49a291408c00f15a04d903948b819cc4d",
4
+ "source": "https://huggingface.co/JujoHotaru/lora/blob/main/sdxl/README.md",
5
+ "entries": [
6
+ {
7
+ "name": "anti25d_xl_v10.safetensors",
8
+ "path": "sdxl/anti25d_xl_v10.safetensors",
9
+ "title": "2.5D→2D変換",
10
+ "doc": "sdxl/anti25d/README.md",
11
+ "image": "sdxl/anti25d/thumb.webp",
12
+ "group_sample": true,
13
+ "size": 93012288
14
+ },
15
+ {
16
+ "name": "make25d_xl_v10.safetensors",
17
+ "path": "sdxl/make25d_xl_v10.safetensors",
18
+ "title": "2D→2.5D変換",
19
+ "doc": "sdxl/make25d/README.md",
20
+ "image": "sdxl/make25d/thumb.webp",
21
+ "group_sample": true,
22
+ "size": 93012288
23
+ },
24
+ {
25
+ "name": "doyagao_xl_v20.safetensors",
26
+ "path": "sdxl/doyagao_xl_v20.safetensors",
27
+ "title": "V字形眉(ドヤ顔など)",
28
+ "doc": "sdxl/doyagao/README.md",
29
+ "image": "sdxl/doyagao/doyagao_xl_v20_01.webp",
30
+ "group_sample": false,
31
+ "size": 21581260
32
+ },
33
+ {
34
+ "name": "talkmouth_xl_A_type1_v20.safetensors",
35
+ "path": "sdxl/talkmouth_xl_A_type1_v20.safetensors",
36
+ "title": "あいうえお発音の口",
37
+ "doc": "sdxl/talkmouth/README.md",
38
+ "image": "sdxl/talkmouth/talkmouth_xl_A_type1_v20.webp",
39
+ "group_sample": false,
40
+ "size": 21581260
41
+ },
42
+ {
43
+ "name": "talkmouth_xl_A_type2_v20.safetensors",
44
+ "path": "sdxl/talkmouth_xl_A_type2_v20.safetensors",
45
+ "title": "あいうえお発音の口",
46
+ "doc": "sdxl/talkmouth/README.md",
47
+ "image": "sdxl/talkmouth/talkmouth_xl_A_type2_v20.webp",
48
+ "group_sample": false,
49
+ "size": 21581260
50
+ },
51
+ {
52
+ "name": "talkmouth_xl_E_type1_v20.safetensors",
53
+ "path": "sdxl/talkmouth_xl_E_type1_v20.safetensors",
54
+ "title": "あいうえお発音の口",
55
+ "doc": "sdxl/talkmouth/README.md",
56
+ "image": "sdxl/talkmouth/talkmouth_xl_E_type1_v20.webp",
57
+ "group_sample": false,
58
+ "size": 21581260
59
+ },
60
+ {
61
+ "name": "talkmouth_xl_E_type2_v20.safetensors",
62
+ "path": "sdxl/talkmouth_xl_E_type2_v20.safetensors",
63
+ "title": "あいうえお発音の口",
64
+ "doc": "sdxl/talkmouth/README.md",
65
+ "image": "sdxl/talkmouth/talkmouth_xl_E_type2_v20.webp",
66
+ "group_sample": false,
67
+ "size": 21581260
68
+ },
69
+ {
70
+ "name": "talkmouth_xl_I_type1_v20.safetensors",
71
+ "path": "sdxl/talkmouth_xl_I_type1_v20.safetensors",
72
+ "title": "あいうえお発音の口",
73
+ "doc": "sdxl/talkmouth/README.md",
74
+ "image": "sdxl/talkmouth/talkmouth_xl_I_type1_v20.webp",
75
+ "group_sample": false,
76
+ "size": 21581260
77
+ },
78
+ {
79
+ "name": "talkmouth_xl_I_type2_v20.safetensors",
80
+ "path": "sdxl/talkmouth_xl_I_type2_v20.safetensors",
81
+ "title": "あいうえお発音の口",
82
+ "doc": "sdxl/talkmouth/README.md",
83
+ "image": "sdxl/talkmouth/talkmouth_xl_I_type2_v20.webp",
84
+ "group_sample": false,
85
+ "size": 21581260
86
+ },
87
+ {
88
+ "name": "talkmouth_xl_O_type1_v20.safetensors",
89
+ "path": "sdxl/talkmouth_xl_O_type1_v20.safetensors",
90
+ "title": "あいうえお発音の口",
91
+ "doc": "sdxl/talkmouth/README.md",
92
+ "image": "sdxl/talkmouth/talkmouth_xl_O_type1_v20.webp",
93
+ "group_sample": false,
94
+ "size": 21581260
95
+ },
96
+ {
97
+ "name": "talkmouth_xl_O_type2_v20.safetensors",
98
+ "path": "sdxl/talkmouth_xl_O_type2_v20.safetensors",
99
+ "title": "あいうえお発音の口",
100
+ "doc": "sdxl/talkmouth/README.md",
101
+ "image": "sdxl/talkmouth/talkmouth_xl_O_type2_v20.webp",
102
+ "group_sample": false,
103
+ "size": 21581260
104
+ },
105
+ {
106
+ "name": "talkmouth_xl_U_type1_v20.safetensors",
107
+ "path": "sdxl/talkmouth_xl_U_type1_v20.safetensors",
108
+ "title": "あいうえお発音の口",
109
+ "doc": "sdxl/talkmouth/README.md",
110
+ "image": "sdxl/talkmouth/talkmouth_xl_U_type1_v20.webp",
111
+ "group_sample": false,
112
+ "size": 21581260
113
+ },
114
+ {
115
+ "name": "talkmouth_xl_U_type2_v20.safetensors",
116
+ "path": "sdxl/talkmouth_xl_U_type2_v20.safetensors",
117
+ "title": "あいうえお発音の口",
118
+ "doc": "sdxl/talkmouth/README.md",
119
+ "image": "sdxl/talkmouth/talkmouth_xl_U_type2_v20.webp",
120
+ "group_sample": false,
121
+ "size": 21581260
122
+ },
123
+ {
124
+ "name": "hotarumouth01_xl_v10.safetensors",
125
+ "path": "sdxl/hotarumouth01_xl_v10.safetensors",
126
+ "title": "いろいろな口の形",
127
+ "doc": "sdxl/mouthcollection/README.md",
128
+ "image": "sdxl/mouthcollection/hotarumouth01_xl_v10.webp",
129
+ "group_sample": false,
130
+ "size": 93012288
131
+ },
132
+ {
133
+ "name": "hotarumouth02_xl_v10.safetensors",
134
+ "path": "sdxl/hotarumouth02_xl_v10.safetensors",
135
+ "title": "いろいろな口の形",
136
+ "doc": "sdxl/mouthcollection/README.md",
137
+ "image": "sdxl/mouthcollection/hotarumouth02_xl_v10.webp",
138
+ "group_sample": false,
139
+ "size": 93012288
140
+ },
141
+ {
142
+ "name": "hotarumouth03_xl_v10.safetensors",
143
+ "path": "sdxl/hotarumouth03_xl_v10.safetensors",
144
+ "title": "いろいろな口の形",
145
+ "doc": "sdxl/mouthcollection/README.md",
146
+ "image": "sdxl/mouthcollection/hotarumouth03_xl_v10.webp",
147
+ "group_sample": false,
148
+ "size": 93012288
149
+ },
150
+ {
151
+ "name": "hotarumouth04_xl_v10.safetensors",
152
+ "path": "sdxl/hotarumouth04_xl_v10.safetensors",
153
+ "title": "いろいろな口の形",
154
+ "doc": "sdxl/mouthcollection/README.md",
155
+ "image": "sdxl/mouthcollection/hotarumouth04_xl_v10.webp",
156
+ "group_sample": false,
157
+ "size": 93012288
158
+ },
159
+ {
160
+ "name": "hotarumouth05_xl_v10.safetensors",
161
+ "path": "sdxl/hotarumouth05_xl_v10.safetensors",
162
+ "title": "いろいろな口の形",
163
+ "doc": "sdxl/mouthcollection/README.md",
164
+ "image": "sdxl/mouthcollection/hotarumouth05_xl_v10.webp",
165
+ "group_sample": false,
166
+ "size": 93012288
167
+ },
168
+ {
169
+ "name": "hotarumouth06_xl_v10.safetensors",
170
+ "path": "sdxl/hotarumouth06_xl_v10.safetensors",
171
+ "title": "いろいろな口の形",
172
+ "doc": "sdxl/mouthcollection/README.md",
173
+ "image": "sdxl/mouthcollection/hotarumouth06_xl_v10.webp",
174
+ "group_sample": false,
175
+ "size": 93012288
176
+ },
177
+ {
178
+ "name": "hotarumouth07_xl_v10.safetensors",
179
+ "path": "sdxl/hotarumouth07_xl_v10.safetensors",
180
+ "title": "いろいろな口の形",
181
+ "doc": "sdxl/mouthcollection/README.md",
182
+ "image": "sdxl/mouthcollection/hotarumouth07_xl_v10.webp",
183
+ "group_sample": false,
184
+ "size": 93012288
185
+ },
186
+ {
187
+ "name": "hotarumouth08_xl_v10.safetensors",
188
+ "path": "sdxl/hotarumouth08_xl_v10.safetensors",
189
+ "title": "いろいろな口の形",
190
+ "doc": "sdxl/mouthcollection/README.md",
191
+ "image": "sdxl/mouthcollection/hotarumouth08_xl_v10.webp",
192
+ "group_sample": false,
193
+ "size": 93012288
194
+ },
195
+ {
196
+ "name": "hotarumouth_xl_smug_v20.safetensors",
197
+ "path": "sdxl/hotarumouth_xl_smug_v20.safetensors",
198
+ "title": "にやけた印象の口",
199
+ "doc": "sdxl/smugmouth/README.md",
200
+ "image": "sdxl/smugmouth/hotarumouth_xl_smug_v20_00.webp",
201
+ "group_sample": false,
202
+ "size": 42878796
203
+ },
204
+ {
205
+ "name": "hotarumouth_xl_smirk1_v20.safetensors",
206
+ "path": "sdxl/hotarumouth_xl_smirk1_v20.safetensors",
207
+ "title": "にやにやした口",
208
+ "doc": "sdxl/smirkmouth/README.md",
209
+ "image": "sdxl/smirkmouth/hotarumouth_xl_smirk1_v20_1.webp",
210
+ "group_sample": false,
211
+ "size": 21581260
212
+ },
213
+ {
214
+ "name": "hotarumouth_xl_smirk2_v20.safetensors",
215
+ "path": "sdxl/hotarumouth_xl_smirk2_v20.safetensors",
216
+ "title": "にやにやした口",
217
+ "doc": "sdxl/smirkmouth/README.md",
218
+ "image": "sdxl/smirkmouth/hotarumouth_xl_smirk2_v20_1.webp",
219
+ "group_sample": false,
220
+ "size": 21581260
221
+ },
222
+ {
223
+ "name": "hotarumouth_xl_smirk_v10.safetensors",
224
+ "path": "sdxl/hotarumouth_xl_smirk_v10.safetensors",
225
+ "title": "にやにやした口",
226
+ "doc": "sdxl/smirkmouth/README.md",
227
+ "image": "sdxl/smirkmouth/hotarumouth_xl_smirk_v10.webp",
228
+ "group_sample": false,
229
+ "size": 98705896
230
+ },
231
+ {
232
+ "name": "hotarueye_xl_smirking_v20.safetensors",
233
+ "path": "sdxl/hotarueye_xl_smirking_v20.safetensors",
234
+ "title": "にやにやした目",
235
+ "doc": "sdxl/smirkingeye/README.md",
236
+ "image": "sdxl/smirkingeye/hotarueye_xl_smirking_v20_01.webp",
237
+ "group_sample": false,
238
+ "size": 21581260
239
+ },
240
+ {
241
+ "name": "eyecolle_xl_code115.safetensors",
242
+ "path": "sdxl/eyecollexl/eyecolle_xl_code115.safetensors",
243
+ "title": "アイコレクションXL",
244
+ "doc": "sdxl/eyecollexl/README.md",
245
+ "image": "sdxl/eyecollexl/tsurime_ill115.webp",
246
+ "group_sample": false,
247
+ "size": 93012288
248
+ },
249
+ {
250
+ "name": "eyecolle_xl_code124.safetensors",
251
+ "path": "sdxl/eyecollexl/eyecolle_xl_code124.safetensors",
252
+ "title": "アイコレクションXL",
253
+ "doc": "sdxl/eyecollexl/README.md",
254
+ "image": "sdxl/eyecollexl/tsurime_ill124.webp",
255
+ "group_sample": false,
256
+ "size": 93012288
257
+ },
258
+ {
259
+ "name": "eyecolle_xl_code125.safetensors",
260
+ "path": "sdxl/eyecollexl/eyecolle_xl_code125.safetensors",
261
+ "title": "アイコレクションXL",
262
+ "doc": "sdxl/eyecollexl/README.md",
263
+ "image": "sdxl/eyecollexl/tsurime_ill125.webp",
264
+ "group_sample": false,
265
+ "size": 93012288
266
+ },
267
+ {
268
+ "name": "eyecolle_xl_code127.safetensors",
269
+ "path": "sdxl/eyecollexl/eyecolle_xl_code127.safetensors",
270
+ "title": "アイコレクションXL",
271
+ "doc": "sdxl/eyecollexl/README.md",
272
+ "image": "sdxl/eyecollexl/tareme_ill127.webp",
273
+ "group_sample": false,
274
+ "size": 93012288
275
+ },
276
+ {
277
+ "name": "eyecolle_xl_code136.safetensors",
278
+ "path": "sdxl/eyecollexl/eyecolle_xl_code136.safetensors",
279
+ "title": "アイコレクションXL",
280
+ "doc": "sdxl/eyecollexl/README.md",
281
+ "image": "sdxl/eyecollexl/tsurime_ill136.webp",
282
+ "group_sample": false,
283
+ "size": 93012288
284
+ },
285
+ {
286
+ "name": "eyecolle_xl_code147.safetensors",
287
+ "path": "sdxl/eyecollexl/eyecolle_xl_code147.safetensors",
288
+ "title": "アイコレクションXL",
289
+ "doc": "sdxl/eyecollexl/README.md",
290
+ "image": "sdxl/eyecollexl/tsurime_ill147.webp",
291
+ "group_sample": false,
292
+ "size": 93012288
293
+ },
294
+ {
295
+ "name": "eyecolle_xl_code150.safetensors",
296
+ "path": "sdxl/eyecollexl/eyecolle_xl_code150.safetensors",
297
+ "title": "アイコレクションXL",
298
+ "doc": "sdxl/eyecollexl/README.md",
299
+ "image": "sdxl/eyecollexl/tareme_ill150.webp",
300
+ "group_sample": false,
301
+ "size": 93012288
302
+ },
303
+ {
304
+ "name": "eyecolle_xl_code152.safetensors",
305
+ "path": "sdxl/eyecollexl/eyecolle_xl_code152.safetensors",
306
+ "title": "アイコレクションXL",
307
+ "doc": "sdxl/eyecollexl/README.md",
308
+ "image": "sdxl/eyecollexl/tsurime_ill152.webp",
309
+ "group_sample": false,
310
+ "size": 93012288
311
+ },
312
+ {
313
+ "name": "eyecolle_xl_code172.safetensors",
314
+ "path": "sdxl/eyecollexl/eyecolle_xl_code172.safetensors",
315
+ "title": "アイコレクションXL",
316
+ "doc": "sdxl/eyecollexl/README.md",
317
+ "image": "sdxl/eyecollexl/tsurime_ill172.webp",
318
+ "group_sample": false,
319
+ "size": 93012288
320
+ },
321
+ {
322
+ "name": "eyecolle_xl_code178.safetensors",
323
+ "path": "sdxl/eyecollexl/eyecolle_xl_code178.safetensors",
324
+ "title": "アイコレクションXL",
325
+ "doc": "sdxl/eyecollexl/README.md",
326
+ "image": "sdxl/eyecollexl/tsurime_ill178.webp",
327
+ "group_sample": false,
328
+ "size": 93012288
329
+ },
330
+ {
331
+ "name": "eyecolle_xl_code185.safetensors",
332
+ "path": "sdxl/eyecollexl/eyecolle_xl_code185.safetensors",
333
+ "title": "アイコレクションXL",
334
+ "doc": "sdxl/eyecollexl/README.md",
335
+ "image": "sdxl/eyecollexl/tsurime_ill185.webp",
336
+ "group_sample": false,
337
+ "size": 93012288
338
+ },
339
+ {
340
+ "name": "eyecolle_xl_code188.safetensors",
341
+ "path": "sdxl/eyecollexl/eyecolle_xl_code188.safetensors",
342
+ "title": "アイコレクションXL",
343
+ "doc": "sdxl/eyecollexl/README.md",
344
+ "image": "sdxl/eyecollexl/tsurime_ill188.webp",
345
+ "group_sample": false,
346
+ "size": 93012288
347
+ },
348
+ {
349
+ "name": "eyecolle_xl_code191.safetensors",
350
+ "path": "sdxl/eyecollexl/eyecolle_xl_code191.safetensors",
351
+ "title": "アイコレクションXL",
352
+ "doc": "sdxl/eyecollexl/README.md",
353
+ "image": "sdxl/eyecollexl/tareme_ill191.webp",
354
+ "group_sample": false,
355
+ "size": 93012288
356
+ },
357
+ {
358
+ "name": "eyecolle_xl_code198.safetensors",
359
+ "path": "sdxl/eyecollexl/eyecolle_xl_code198.safetensors",
360
+ "title": "アイコレクションXL",
361
+ "doc": "sdxl/eyecollexl/README.md",
362
+ "image": "sdxl/eyecollexl/tsurime_ill198.webp",
363
+ "group_sample": false,
364
+ "size": 93012288
365
+ },
366
+ {
367
+ "name": "eyecolle_xl_code202.safetensors",
368
+ "path": "sdxl/eyecollexl/eyecolle_xl_code202.safetensors",
369
+ "title": "アイコレクションXL",
370
+ "doc": "sdxl/eyecollexl/README.md",
371
+ "image": "sdxl/eyecollexl/tsurime_ill202.webp",
372
+ "group_sample": false,
373
+ "size": 93012288
374
+ },
375
+ {
376
+ "name": "eyecolle_xl_code206.safetensors",
377
+ "path": "sdxl/eyecollexl/eyecolle_xl_code206.safetensors",
378
+ "title": "アイコレクションXL",
379
+ "doc": "sdxl/eyecollexl/README.md",
380
+ "image": "sdxl/eyecollexl/tareme_ill206.webp",
381
+ "group_sample": false,
382
+ "size": 93012288
383
+ },
384
+ {
385
+ "name": "eyecolle_xl_code208.safetensors",
386
+ "path": "sdxl/eyecollexl/eyecolle_xl_code208.safetensors",
387
+ "title": "アイコレクションXL",
388
+ "doc": "sdxl/eyecollexl/README.md",
389
+ "image": "sdxl/eyecollexl/tsurime_ill208.webp",
390
+ "group_sample": false,
391
+ "size": 93012288
392
+ },
393
+ {
394
+ "name": "eyecolle_xl_code222.safetensors",
395
+ "path": "sdxl/eyecollexl/eyecolle_xl_code222.safetensors",
396
+ "title": "アイコレクションXL",
397
+ "doc": "sdxl/eyecollexl/README.md",
398
+ "image": "sdxl/eyecollexl/tareme_ill222.webp",
399
+ "group_sample": false,
400
+ "size": 93012288
401
+ },
402
+ {
403
+ "name": "eyecolle_xl_code223.safetensors",
404
+ "path": "sdxl/eyecollexl/eyecolle_xl_code223.safetensors",
405
+ "title": "アイコレクションXL",
406
+ "doc": "sdxl/eyecollexl/README.md",
407
+ "image": "sdxl/eyecollexl/tsurime_ill223.webp",
408
+ "group_sample": false,
409
+ "size": 93012288
410
+ },
411
+ {
412
+ "name": "eyecolle_xl_code228.safetensors",
413
+ "path": "sdxl/eyecollexl/eyecolle_xl_code228.safetensors",
414
+ "title": "アイコレクションXL",
415
+ "doc": "sdxl/eyecollexl/README.md",
416
+ "image": "sdxl/eyecollexl/tsurime_ill228.webp",
417
+ "group_sample": false,
418
+ "size": 93012288
419
+ },
420
+ {
421
+ "name": "eyecolle_xl_code237.safetensors",
422
+ "path": "sdxl/eyecollexl/eyecolle_xl_code237.safetensors",
423
+ "title": "アイコレクションXL",
424
+ "doc": "sdxl/eyecollexl/README.md",
425
+ "image": "sdxl/eyecollexl/tsurime_ill237.webp",
426
+ "group_sample": false,
427
+ "size": 93012288
428
+ },
429
+ {
430
+ "name": "eyecolle_xl_code238.safetensors",
431
+ "path": "sdxl/eyecollexl/eyecolle_xl_code238.safetensors",
432
+ "title": "アイコレクションXL",
433
+ "doc": "sdxl/eyecollexl/README.md",
434
+ "image": "sdxl/eyecollexl/tsurime_ill238.webp",
435
+ "group_sample": false,
436
+ "size": 93012288
437
+ },
438
+ {
439
+ "name": "eyecolle_xl_code241.safetensors",
440
+ "path": "sdxl/eyecollexl/eyecolle_xl_code241.safetensors",
441
+ "title": "アイコレクションXL",
442
+ "doc": "sdxl/eyecollexl/README.md",
443
+ "image": "sdxl/eyecollexl/tareme_ill241.webp",
444
+ "group_sample": false,
445
+ "size": 93012288
446
+ },
447
+ {
448
+ "name": "eyecolle_xl_code242.safetensors",
449
+ "path": "sdxl/eyecollexl/eyecolle_xl_code242.safetensors",
450
+ "title": "アイコレクションXL",
451
+ "doc": "sdxl/eyecollexl/README.md",
452
+ "image": "sdxl/eyecollexl/tareme_ill242.webp",
453
+ "group_sample": false,
454
+ "size": 93012288
455
+ },
456
+ {
457
+ "name": "eyecolle_xl_code243.safetensors",
458
+ "path": "sdxl/eyecollexl/eyecolle_xl_code243.safetensors",
459
+ "title": "アイコレクションXL",
460
+ "doc": "sdxl/eyecollexl/README.md",
461
+ "image": "sdxl/eyecollexl/tareme_ill243.webp",
462
+ "group_sample": false,
463
+ "size": 93012288
464
+ },
465
+ {
466
+ "name": "eyecolle_xl_code244.safetensors",
467
+ "path": "sdxl/eyecollexl/eyecolle_xl_code244.safetensors",
468
+ "title": "アイコレクションXL",
469
+ "doc": "sdxl/eyecollexl/README.md",
470
+ "image": "sdxl/eyecollexl/tareme_ill244.webp",
471
+ "group_sample": false,
472
+ "size": 93012288
473
+ },
474
+ {
475
+ "name": "eyecolle_xl_code245.safetensors",
476
+ "path": "sdxl/eyecollexl/eyecolle_xl_code245.safetensors",
477
+ "title": "アイコレクションXL",
478
+ "doc": "sdxl/eyecollexl/README.md",
479
+ "image": "sdxl/eyecollexl/tareme_ill245.webp",
480
+ "group_sample": false,
481
+ "size": 93012288
482
+ },
483
+ {
484
+ "name": "eyecolle_xl_code246.safetensors",
485
+ "path": "sdxl/eyecollexl/eyecolle_xl_code246.safetensors",
486
+ "title": "アイコレクションXL",
487
+ "doc": "sdxl/eyecollexl/README.md",
488
+ "image": "sdxl/eyecollexl/tareme_ill246.webp",
489
+ "group_sample": false,
490
+ "size": 93012288
491
+ },
492
+ {
493
+ "name": "eyecolle_xl_code247.safetensors",
494
+ "path": "sdxl/eyecollexl/eyecolle_xl_code247.safetensors",
495
+ "title": "アイコレクションXL",
496
+ "doc": "sdxl/eyecollexl/README.md",
497
+ "image": "sdxl/eyecollexl/tareme_ill247.webp",
498
+ "group_sample": false,
499
+ "size": 93012288
500
+ },
501
+ {
502
+ "name": "eyecolle_xl_code248.safetensors",
503
+ "path": "sdxl/eyecollexl/eyecolle_xl_code248.safetensors",
504
+ "title": "アイコレクションXL",
505
+ "doc": "sdxl/eyecollexl/README.md",
506
+ "image": "sdxl/eyecollexl/tsurime_ill248.webp",
507
+ "group_sample": false,
508
+ "size": 93012288
509
+ },
510
+ {
511
+ "name": "eyecolle_xl_code249.safetensors",
512
+ "path": "sdxl/eyecollexl/eyecolle_xl_code249.safetensors",
513
+ "title": "アイコレクションXL",
514
+ "doc": "sdxl/eyecollexl/README.md",
515
+ "image": "sdxl/eyecollexl/tareme_ill249.webp",
516
+ "group_sample": false,
517
+ "size": 93012288
518
+ },
519
+ {
520
+ "name": "eyecolle_xl_code250.safetensors",
521
+ "path": "sdxl/eyecollexl/eyecolle_xl_code250.safetensors",
522
+ "title": "アイコレクションXL",
523
+ "doc": "sdxl/eyecollexl/README.md",
524
+ "image": "sdxl/eyecollexl/tsurime_ill250.webp",
525
+ "group_sample": false,
526
+ "size": 93012288
527
+ },
528
+ {
529
+ "name": "eyecolle_xl_code251.safetensors",
530
+ "path": "sdxl/eyecollexl/eyecolle_xl_code251.safetensors",
531
+ "title": "アイコレクションXL",
532
+ "doc": "sdxl/eyecollexl/README.md",
533
+ "image": "sdxl/eyecollexl/tareme_ill251.webp",
534
+ "group_sample": false,
535
+ "size": 93012288
536
+ },
537
+ {
538
+ "name": "eyecolle_xl_code252.safetensors",
539
+ "path": "sdxl/eyecollexl/eyecolle_xl_code252.safetensors",
540
+ "title": "アイコレクションXL",
541
+ "doc": "sdxl/eyecollexl/README.md",
542
+ "image": "sdxl/eyecollexl/tsurime_ill252.webp",
543
+ "group_sample": false,
544
+ "size": 93012288
545
+ },
546
+ {
547
+ "name": "eyecolle_xl_code253.safetensors",
548
+ "path": "sdxl/eyecollexl/eyecolle_xl_code253.safetensors",
549
+ "title": "アイコレクションXL",
550
+ "doc": "sdxl/eyecollexl/README.md",
551
+ "image": "sdxl/eyecollexl/tsurime_ill253.webp",
552
+ "group_sample": false,
553
+ "size": 93012288
554
+ },
555
+ {
556
+ "name": "eyecolle_xl_code254.safetensors",
557
+ "path": "sdxl/eyecollexl/eyecolle_xl_code254.safetensors",
558
+ "title": "アイコレクションXL",
559
+ "doc": "sdxl/eyecollexl/README.md",
560
+ "image": "sdxl/eyecollexl/tsurime_ill254.webp",
561
+ "group_sample": false,
562
+ "size": 93012288
563
+ },
564
+ {
565
+ "name": "eyecolle_xl_code255.safetensors",
566
+ "path": "sdxl/eyecollexl/eyecolle_xl_code255.safetensors",
567
+ "title": "アイコレクションXL",
568
+ "doc": "sdxl/eyecollexl/README.md",
569
+ "image": "sdxl/eyecollexl/tareme_ill255.webp",
570
+ "group_sample": false,
571
+ "size": 93012288
572
+ },
573
+ {
574
+ "name": "eyecolle_xl_code256.safetensors",
575
+ "path": "sdxl/eyecollexl/eyecolle_xl_code256.safetensors",
576
+ "title": "アイコレクションXL",
577
+ "doc": "sdxl/eyecollexl/README.md",
578
+ "image": "sdxl/eyecollexl/tareme_ill256.webp",
579
+ "group_sample": false,
580
+ "size": 93012288
581
+ },
582
+ {
583
+ "name": "eyecolle_xl_code257.safetensors",
584
+ "path": "sdxl/eyecollexl/eyecolle_xl_code257.safetensors",
585
+ "title": "アイコレクションXL",
586
+ "doc": "sdxl/eyecollexl/README.md",
587
+ "image": "sdxl/eyecollexl/tareme_ill257.webp",
588
+ "group_sample": false,
589
+ "size": 93012288
590
+ },
591
+ {
592
+ "name": "eyecolle_xl_code258.safetensors",
593
+ "path": "sdxl/eyecollexl/eyecolle_xl_code258.safetensors",
594
+ "title": "アイコレクションXL",
595
+ "doc": "sdxl/eyecollexl/README.md",
596
+ "image": "sdxl/eyecollexl/tsurime_ill258.webp",
597
+ "group_sample": false,
598
+ "size": 93012288
599
+ },
600
+ {
601
+ "name": "eyecolle_xl_code259.safetensors",
602
+ "path": "sdxl/eyecollexl/eyecolle_xl_code259.safetensors",
603
+ "title": "アイコレクションXL",
604
+ "doc": "sdxl/eyecollexl/README.md",
605
+ "image": "sdxl/eyecollexl/tareme_ill259.webp",
606
+ "group_sample": false,
607
+ "size": 93012288
608
+ },
609
+ {
610
+ "name": "eyecolle_xl_code260.safetensors",
611
+ "path": "sdxl/eyecollexl/eyecolle_xl_code260.safetensors",
612
+ "title": "アイコレクションXL",
613
+ "doc": "sdxl/eyecollexl/README.md",
614
+ "image": "sdxl/eyecollexl/tareme_ill260.webp",
615
+ "group_sample": false,
616
+ "size": 93012288
617
+ },
618
+ {
619
+ "name": "eyecolle_xl_code261.safetensors",
620
+ "path": "sdxl/eyecollexl/eyecolle_xl_code261.safetensors",
621
+ "title": "アイコレクションXL",
622
+ "doc": "sdxl/eyecollexl/README.md",
623
+ "image": "sdxl/eyecollexl/tsurime_ill261.webp",
624
+ "group_sample": false,
625
+ "size": 93012288
626
+ },
627
+ {
628
+ "name": "eyecolle_xl_code262.safetensors",
629
+ "path": "sdxl/eyecollexl/eyecolle_xl_code262.safetensors",
630
+ "title": "アイコレクションXL",
631
+ "doc": "sdxl/eyecollexl/README.md",
632
+ "image": "sdxl/eyecollexl/tareme_ill262.webp",
633
+ "group_sample": false,
634
+ "size": 93012288
635
+ },
636
+ {
637
+ "name": "eyecolle_xl_code504.safetensors",
638
+ "path": "sdxl/eyecollexl/eyecolle_xl_code504.safetensors",
639
+ "title": "アイコレクションXL",
640
+ "doc": "sdxl/eyecollexl/README.md",
641
+ "image": "sdxl/eyecollexl/tsurime_ill504.webp",
642
+ "group_sample": false,
643
+ "size": 93012288
644
+ },
645
+ {
646
+ "name": "eyecolle_xl_code505.safetensors",
647
+ "path": "sdxl/eyecollexl/eyecolle_xl_code505.safetensors",
648
+ "title": "アイコレクションXL",
649
+ "doc": "sdxl/eyecollexl/README.md",
650
+ "image": "sdxl/eyecollexl/tareme_ill505.webp",
651
+ "group_sample": false,
652
+ "size": 93012288
653
+ },
654
+ {
655
+ "name": "eyecolle_xl_code701.safetensors",
656
+ "path": "sdxl/eyecollexl/eyecolle_xl_code701.safetensors",
657
+ "title": "アイコレクションXL",
658
+ "doc": "sdxl/eyecollexl/README.md",
659
+ "image": "sdxl/eyecollexl/tareme_ill701.webp",
660
+ "group_sample": false,
661
+ "size": 93012288
662
+ },
663
+ {
664
+ "name": "eyecolle_xl_code702.safetensors",
665
+ "path": "sdxl/eyecollexl/eyecolle_xl_code702.safetensors",
666
+ "title": "アイコレクションXL",
667
+ "doc": "sdxl/eyecollexl/README.md",
668
+ "image": "sdxl/eyecollexl/tareme_ill702.webp",
669
+ "group_sample": false,
670
+ "size": 93012288
671
+ },
672
+ {
673
+ "name": "eyecolle_xl_code703.safetensors",
674
+ "path": "sdxl/eyecollexl/eyecolle_xl_code703.safetensors",
675
+ "title": "アイコレクションXL",
676
+ "doc": "sdxl/eyecollexl/README.md",
677
+ "image": "sdxl/eyecollexl/tareme_ill703.webp",
678
+ "group_sample": false,
679
+ "size": 93012288
680
+ },
681
+ {
682
+ "name": "eyecolle_xl_code704.safetensors",
683
+ "path": "sdxl/eyecollexl/eyecolle_xl_code704.safetensors",
684
+ "title": "アイコレクションXL",
685
+ "doc": "sdxl/eyecollexl/README.md",
686
+ "image": "sdxl/eyecollexl/tareme_ill704.webp",
687
+ "group_sample": false,
688
+ "size": 93012288
689
+ },
690
+ {
691
+ "name": "eyecolle_xl_code705.safetensors",
692
+ "path": "sdxl/eyecollexl/eyecolle_xl_code705.safetensors",
693
+ "title": "アイコレクションXL",
694
+ "doc": "sdxl/eyecollexl/README.md",
695
+ "image": "sdxl/eyecollexl/tareme_ill705.webp",
696
+ "group_sample": false,
697
+ "size": 93012288
698
+ },
699
+ {
700
+ "name": "eyecolle_xl_code706.safetensors",
701
+ "path": "sdxl/eyecollexl/eyecolle_xl_code706.safetensors",
702
+ "title": "アイコレクションXL",
703
+ "doc": "sdxl/eyecollexl/README.md",
704
+ "image": "sdxl/eyecollexl/tareme_ill706.webp",
705
+ "group_sample": false,
706
+ "size": 93012288
707
+ },
708
+ {
709
+ "name": "eyecolle_xl_code707.safetensors",
710
+ "path": "sdxl/eyecollexl/eyecolle_xl_code707.safetensors",
711
+ "title": "アイコレクションXL",
712
+ "doc": "sdxl/eyecollexl/README.md",
713
+ "image": "sdxl/eyecollexl/tsurime_ill707.webp",
714
+ "group_sample": false,
715
+ "size": 93012288
716
+ },
717
+ {
718
+ "name": "eyecolle_xl_code708.safetensors",
719
+ "path": "sdxl/eyecollexl/eyecolle_xl_code708.safetensors",
720
+ "title": "アイコレクションXL",
721
+ "doc": "sdxl/eyecollexl/README.md",
722
+ "image": "sdxl/eyecollexl/tsurime_ill708.webp",
723
+ "group_sample": false,
724
+ "size": 93012288
725
+ },
726
+ {
727
+ "name": "eyecolle_xl_code709.safetensors",
728
+ "path": "sdxl/eyecollexl/eyecolle_xl_code709.safetensors",
729
+ "title": "アイコレクションXL",
730
+ "doc": "sdxl/eyecollexl/README.md",
731
+ "image": "sdxl/eyecollexl/tsurime_ill709.webp",
732
+ "group_sample": false,
733
+ "size": 93012288
734
+ },
735
+ {
736
+ "name": "eyecolle_xl_code710.safetensors",
737
+ "path": "sdxl/eyecollexl/eyecolle_xl_code710.safetensors",
738
+ "title": "アイコレクションXL",
739
+ "doc": "sdxl/eyecollexl/README.md",
740
+ "image": "sdxl/eyecollexl/tareme_ill710.webp",
741
+ "group_sample": false,
742
+ "size": 93012288
743
+ },
744
+ {
745
+ "name": "eyecolle_xl_code711.safetensors",
746
+ "path": "sdxl/eyecollexl/eyecolle_xl_code711.safetensors",
747
+ "title": "アイコレクションXL",
748
+ "doc": "sdxl/eyecollexl/README.md",
749
+ "image": "sdxl/eyecollexl/tsurime_ill711.webp",
750
+ "group_sample": false,
751
+ "size": 93012288
752
+ },
753
+ {
754
+ "name": "eyecolle_xl_code712.safetensors",
755
+ "path": "sdxl/eyecollexl/eyecolle_xl_code712.safetensors",
756
+ "title": "アイコレクションXL",
757
+ "doc": "sdxl/eyecollexl/README.md",
758
+ "image": "sdxl/eyecollexl/tsurime_ill712.webp",
759
+ "group_sample": false,
760
+ "size": 93012288
761
+ },
762
+ {
763
+ "name": "eyecolle_xl_code713.safetensors",
764
+ "path": "sdxl/eyecollexl/eyecolle_xl_code713.safetensors",
765
+ "title": "アイコレクションXL",
766
+ "doc": "sdxl/eyecollexl/README.md",
767
+ "image": "sdxl/eyecollexl/tareme_ill713.webp",
768
+ "group_sample": false,
769
+ "size": 93012288
770
+ },
771
+ {
772
+ "name": "eyecolle_xl_code714.safetensors",
773
+ "path": "sdxl/eyecollexl/eyecolle_xl_code714.safetensors",
774
+ "title": "アイコレクションXL",
775
+ "doc": "sdxl/eyecollexl/README.md",
776
+ "image": "sdxl/eyecollexl/tareme_ill714.webp",
777
+ "group_sample": false,
778
+ "size": 93012288
779
+ },
780
+ {
781
+ "name": "eyecolle_xl_code715.safetensors",
782
+ "path": "sdxl/eyecollexl/eyecolle_xl_code715.safetensors",
783
+ "title": "アイコレクションXL",
784
+ "doc": "sdxl/eyecollexl/README.md",
785
+ "image": "sdxl/eyecollexl/tsurime_ill715.webp",
786
+ "group_sample": false,
787
+ "size": 93012288
788
+ },
789
+ {
790
+ "name": "eyecolle_xl_code716.safetensors",
791
+ "path": "sdxl/eyecollexl/eyecolle_xl_code716.safetensors",
792
+ "title": "アイコレクションXL",
793
+ "doc": "sdxl/eyecollexl/README.md",
794
+ "image": "sdxl/eyecollexl/tareme_ill716.webp",
795
+ "group_sample": false,
796
+ "size": 93012288
797
+ },
798
+ {
799
+ "name": "eyecolle_xl_code717.safetensors",
800
+ "path": "sdxl/eyecollexl/eyecolle_xl_code717.safetensors",
801
+ "title": "アイコレクションXL",
802
+ "doc": "sdxl/eyecollexl/README.md",
803
+ "image": "sdxl/eyecollexl/tsurime_ill717.webp",
804
+ "group_sample": false,
805
+ "size": 93012288
806
+ },
807
+ {
808
+ "name": "eyecolle_xl_code718.safetensors",
809
+ "path": "sdxl/eyecollexl/eyecolle_xl_code718.safetensors",
810
+ "title": "アイコレクションXL",
811
+ "doc": "sdxl/eyecollexl/README.md",
812
+ "image": "sdxl/eyecollexl/tsurime_ill718.webp",
813
+ "group_sample": false,
814
+ "size": 93012288
815
+ },
816
+ {
817
+ "name": "eyecolle_xl_code719.safetensors",
818
+ "path": "sdxl/eyecollexl/eyecolle_xl_code719.safetensors",
819
+ "title": "アイコレクションXL",
820
+ "doc": "sdxl/eyecollexl/README.md",
821
+ "image": "sdxl/eyecollexl/tareme_ill719.webp",
822
+ "group_sample": false,
823
+ "size": 93012288
824
+ },
825
+ {
826
+ "name": "eyecolle_xl_code720.safetensors",
827
+ "path": "sdxl/eyecollexl/eyecolle_xl_code720.safetensors",
828
+ "title": "アイコレクションXL",
829
+ "doc": "sdxl/eyecollexl/README.md",
830
+ "image": "sdxl/eyecollexl/tsurime_ill720.webp",
831
+ "group_sample": false,
832
+ "size": 93012288
833
+ },
834
+ {
835
+ "name": "eyecolle_xl_code721.safetensors",
836
+ "path": "sdxl/eyecollexl/eyecolle_xl_code721.safetensors",
837
+ "title": "アイコレクションXL",
838
+ "doc": "sdxl/eyecollexl/README.md",
839
+ "image": "sdxl/eyecollexl/tareme_ill721.webp",
840
+ "group_sample": false,
841
+ "size": 93012288
842
+ },
843
+ {
844
+ "name": "eyecolle_xl_code722.safetensors",
845
+ "path": "sdxl/eyecollexl/eyecolle_xl_code722.safetensors",
846
+ "title": "アイコレクションXL",
847
+ "doc": "sdxl/eyecollexl/README.md",
848
+ "image": "sdxl/eyecollexl/tareme_ill722.webp",
849
+ "group_sample": false,
850
+ "size": 93012288
851
+ },
852
+ {
853
+ "name": "eyecolle_xl_code723.safetensors",
854
+ "path": "sdxl/eyecollexl/eyecolle_xl_code723.safetensors",
855
+ "title": "アイコレクションXL",
856
+ "doc": "sdxl/eyecollexl/README.md",
857
+ "image": "sdxl/eyecollexl/tsurime_ill723.webp",
858
+ "group_sample": false,
859
+ "size": 93012288
860
+ },
861
+ {
862
+ "name": "eyecolle_xl_code724.safetensors",
863
+ "path": "sdxl/eyecollexl/eyecolle_xl_code724.safetensors",
864
+ "title": "アイコレクションXL",
865
+ "doc": "sdxl/eyecollexl/README.md",
866
+ "image": "sdxl/eyecollexl/tareme_ill724.webp",
867
+ "group_sample": false,
868
+ "size": 93012288
869
+ },
870
+ {
871
+ "name": "eyecolle_xl_code725.safetensors",
872
+ "path": "sdxl/eyecollexl/eyecolle_xl_code725.safetensors",
873
+ "title": "アイコレクションXL",
874
+ "doc": "sdxl/eyecollexl/README.md",
875
+ "image": "sdxl/eyecollexl/tareme_ill725.webp",
876
+ "group_sample": false,
877
+ "size": 93012288
878
+ },
879
+ {
880
+ "name": "eyecolle_xl_code726.safetensors",
881
+ "path": "sdxl/eyecollexl/eyecolle_xl_code726.safetensors",
882
+ "title": "アイコレクションXL",
883
+ "doc": "sdxl/eyecollexl/README.md",
884
+ "image": "sdxl/eyecollexl/tsurime_ill726.webp",
885
+ "group_sample": false,
886
+ "size": 93012288
887
+ },
888
+ {
889
+ "name": "eyecolle_xl_code727.safetensors",
890
+ "path": "sdxl/eyecollexl/eyecolle_xl_code727.safetensors",
891
+ "title": "アイコレクションXL",
892
+ "doc": "sdxl/eyecollexl/README.md",
893
+ "image": "sdxl/eyecollexl/tsurime_ill727.webp",
894
+ "group_sample": false,
895
+ "size": 93012288
896
+ },
897
+ {
898
+ "name": "eyecolle_xl_code728.safetensors",
899
+ "path": "sdxl/eyecollexl/eyecolle_xl_code728.safetensors",
900
+ "title": "アイコレクションXL",
901
+ "doc": "sdxl/eyecollexl/README.md",
902
+ "image": "sdxl/eyecollexl/tsurime_ill728.webp",
903
+ "group_sample": false,
904
+ "size": 93012288
905
+ },
906
+ {
907
+ "name": "eyecolle_xl_code729.safetensors",
908
+ "path": "sdxl/eyecollexl/eyecolle_xl_code729.safetensors",
909
+ "title": "アイコレクションXL",
910
+ "doc": "sdxl/eyecollexl/README.md",
911
+ "image": "sdxl/eyecollexl/tsurime_ill729.webp",
912
+ "group_sample": false,
913
+ "size": 93012288
914
+ },
915
+ {
916
+ "name": "eyecolle_xl_code730.safetensors",
917
+ "path": "sdxl/eyecollexl/eyecolle_xl_code730.safetensors",
918
+ "title": "アイコレクションXL",
919
+ "doc": "sdxl/eyecollexl/README.md",
920
+ "image": "sdxl/eyecollexl/tsurime_ill730.webp",
921
+ "group_sample": false,
922
+ "size": 93012288
923
+ },
924
+ {
925
+ "name": "eyecolle_xl_code731.safetensors",
926
+ "path": "sdxl/eyecollexl/eyecolle_xl_code731.safetensors",
927
+ "title": "アイコレクションXL",
928
+ "doc": "sdxl/eyecollexl/README.md",
929
+ "image": "sdxl/eyecollexl/tsurime_ill731.webp",
930
+ "group_sample": false,
931
+ "size": 93012288
932
+ },
933
+ {
934
+ "name": "eyecolle_xl_code732.safetensors",
935
+ "path": "sdxl/eyecollexl/eyecolle_xl_code732.safetensors",
936
+ "title": "アイコレクションXL",
937
+ "doc": "sdxl/eyecollexl/README.md",
938
+ "image": "sdxl/eyecollexl/tsurime_ill732.webp",
939
+ "group_sample": false,
940
+ "size": 93012288
941
+ },
942
+ {
943
+ "name": "eyecolle_xl_code733.safetensors",
944
+ "path": "sdxl/eyecollexl/eyecolle_xl_code733.safetensors",
945
+ "title": "アイコレクションXL",
946
+ "doc": "sdxl/eyecollexl/README.md",
947
+ "image": "sdxl/eyecollexl/tsurime_ill733.webp",
948
+ "group_sample": false,
949
+ "size": 93012288
950
+ },
951
+ {
952
+ "name": "eyecolle_xl_code734.safetensors",
953
+ "path": "sdxl/eyecollexl/eyecolle_xl_code734.safetensors",
954
+ "title": "アイコレクションXL",
955
+ "doc": "sdxl/eyecollexl/README.md",
956
+ "image": "sdxl/eyecollexl/tsurime_ill734.webp",
957
+ "group_sample": false,
958
+ "size": 93012288
959
+ },
960
+ {
961
+ "name": "eyecolle_xl_code735.safetensors",
962
+ "path": "sdxl/eyecollexl/eyecolle_xl_code735.safetensors",
963
+ "title": "アイコレクションXL",
964
+ "doc": "sdxl/eyecollexl/README.md",
965
+ "image": "sdxl/eyecollexl/tsurime_ill735.webp",
966
+ "group_sample": false,
967
+ "size": 93012288
968
+ },
969
+ {
970
+ "name": "wink1_left_xl_v20.safetensors",
971
+ "path": "sdxl/wink1_left_xl_v20.safetensors",
972
+ "title": "ウィンク補助",
973
+ "doc": "sdxl/wink/README.md",
974
+ "image": "sdxl/wink/wink1_left_xl_v20_face.webp",
975
+ "group_sample": false,
976
+ "size": 85438260
977
+ },
978
+ {
979
+ "name": "wink1_right_xl_v20.safetensors",
980
+ "path": "sdxl/wink1_right_xl_v20.safetensors",
981
+ "title": "ウィンク補助",
982
+ "doc": "sdxl/wink/README.md",
983
+ "image": "sdxl/wink/wink1_right_xl_v20_face.webp",
984
+ "group_sample": false,
985
+ "size": 85420556
986
+ },
987
+ {
988
+ "name": "wink2_left_xl_v20.safetensors",
989
+ "path": "sdxl/wink2_left_xl_v20.safetensors",
990
+ "title": "ウィンク補助",
991
+ "doc": "sdxl/wink/README.md",
992
+ "image": "sdxl/wink/wink2_left_xl_v20_face.webp",
993
+ "group_sample": false,
994
+ "size": 87304924
995
+ },
996
+ {
997
+ "name": "wink2_right_xl_v20.safetensors",
998
+ "path": "sdxl/wink2_right_xl_v20.safetensors",
999
+ "title": "ウィンク補助",
1000
+ "doc": "sdxl/wink/README.md",
1001
+ "image": "sdxl/wink/wink2_right_xl_v20_face.webp",
1002
+ "group_sample": false,
1003
+ "size": 85438260
1004
+ },
1005
+ {
1006
+ "name": "hotarueye_xl_jitome1_v20.safetensors",
1007
+ "path": "sdxl/hotarueye_xl_jitome1_v20.safetensors",
1008
+ "title": "ジト目",
1009
+ "doc": "sdxl/jitome/README.md",
1010
+ "image": "sdxl/jitome/hotarueye_xl_jitome1_v20_1_t.webp",
1011
+ "group_sample": false,
1012
+ "size": 42861100
1013
+ },
1014
+ {
1015
+ "name": "hotarueye_xl_jitome2_v20.safetensors",
1016
+ "path": "sdxl/hotarueye_xl_jitome2_v20.safetensors",
1017
+ "title": "ジト目",
1018
+ "doc": "sdxl/jitome/README.md",
1019
+ "image": "sdxl/jitome/hotarueye_xl_jitome2_v20_1_t.webp",
1020
+ "group_sample": false,
1021
+ "size": 42861100
1022
+ },
1023
+ {
1024
+ "name": "hotarueye_xl_jitome3_v20.safetensors",
1025
+ "path": "sdxl/hotarueye_xl_jitome3_v20.safetensors",
1026
+ "title": "ジト目",
1027
+ "doc": "sdxl/jitome/README.md",
1028
+ "image": "sdxl/jitome/hotarueye_xl_jitome3_v20_1_t.webp",
1029
+ "group_sample": false,
1030
+ "size": 42861100
1031
+ },
1032
+ {
1033
+ "name": "hotarueye_xl_jitome4_v20.safetensors",
1034
+ "path": "sdxl/hotarueye_xl_jitome4_v20.safetensors",
1035
+ "title": "ジト目",
1036
+ "doc": "sdxl/jitome/README.md",
1037
+ "image": "sdxl/jitome/hotarueye_xl_jitome4_v20_1_t.webp",
1038
+ "group_sample": false,
1039
+ "size": 42861100
1040
+ },
1041
+ {
1042
+ "name": "hotarueye_xl_tareme1_v20.safetensors",
1043
+ "path": "sdxl/hotarueye_xl_tareme1_v20.safetensors",
1044
+ "title": "タレ目",
1045
+ "doc": "sdxl/tareme/README.md",
1046
+ "image": "sdxl/tareme/hotarueye_xl_tareme1_v20_1_t.webp",
1047
+ "group_sample": false,
1048
+ "size": 42861100
1049
+ },
1050
+ {
1051
+ "name": "hotarueye_xl_tareme2_v20.safetensors",
1052
+ "path": "sdxl/hotarueye_xl_tareme2_v20.safetensors",
1053
+ "title": "タレ目",
1054
+ "doc": "sdxl/tareme/README.md",
1055
+ "image": "sdxl/tareme/hotarueye_xl_tareme2_v20_1_t.webp",
1056
+ "group_sample": false,
1057
+ "size": 42861100
1058
+ },
1059
+ {
1060
+ "name": "hotarueye_xl_tareme3_v20.safetensors",
1061
+ "path": "sdxl/hotarueye_xl_tareme3_v20.safetensors",
1062
+ "title": "タレ目",
1063
+ "doc": "sdxl/tareme/README.md",
1064
+ "image": "sdxl/tareme/hotarueye_xl_tareme3_v20_1_t.webp",
1065
+ "group_sample": false,
1066
+ "size": 42861100
1067
+ },
1068
+ {
1069
+ "name": "hotarueye_xl_tareme4_v20.safetensors",
1070
+ "path": "sdxl/hotarueye_xl_tareme4_v20.safetensors",
1071
+ "title": "タレ目",
1072
+ "doc": "sdxl/tareme/README.md",
1073
+ "image": "sdxl/tareme/hotarueye_xl_tareme4_v20_1_t.webp",
1074
+ "group_sample": false,
1075
+ "size": 42861100
1076
+ },
1077
+ {
1078
+ "name": "hotarueye_xl_comic01_v20_ill.safetensors",
1079
+ "path": "sdxl/hotarueye_xl_comic01_v20_ill.safetensors",
1080
+ "title": "デフォル眼 Type1",
1081
+ "doc": "sdxl/README.md",
1082
+ "image": "sdxl/hotarueye_xl_comic01_v20.webp",
1083
+ "group_sample": false,
1084
+ "size": 42861100
1085
+ },
1086
+ {
1087
+ "name": "hotarueye_xl_comic04_v10_ill.safetensors",
1088
+ "path": "sdxl/hotarueye_xl_comic04_v10_ill.safetensors",
1089
+ "title": "デフォル眼 Type4",
1090
+ "doc": "sdxl/README.md",
1091
+ "image": "sdxl/hotarueye_xl_comic04_v10.webp",
1092
+ "group_sample": false,
1093
+ "size": 42878796
1094
+ },
1095
+ {
1096
+ "name": "inv_v_eyebrows_xl_v20.safetensors",
1097
+ "path": "sdxl/inv_v_eyebrows_xl_v20.safetensors",
1098
+ "title": "ハの字型の眉",
1099
+ "doc": "sdxl/inv_v_eyebrows/README.md",
1100
+ "image": "sdxl/inv_v_eyebrows/inv_v_eyebrows_xl_v20_01.webp",
1101
+ "group_sample": false,
1102
+ "size": 21581260
1103
+ },
1104
+ {
1105
+ "name": "coldeyes_xl_v10.safetensors",
1106
+ "path": "sdxl/coldeyes_xl_v10.safetensors",
1107
+ "title": "不機嫌そうなジト目",
1108
+ "doc": "sdxl/coldeyes/README.md",
1109
+ "image": "sdxl/coldeyes/coldeyes_xl_v10_01.webp",
1110
+ "group_sample": false,
1111
+ "size": 42861100
1112
+ },
1113
+ {
1114
+ "name": "hotarumouth_xl_circle1_v20.safetensors",
1115
+ "path": "sdxl/hotarumouth_xl_circle1_v20.safetensors",
1116
+ "title": "丸い口",
1117
+ "doc": "sdxl/circlemouth/README.md",
1118
+ "image": "sdxl/circlemouth/hotarumouth_xl_circle1_v20_1.webp",
1119
+ "group_sample": false,
1120
+ "size": 21581260
1121
+ },
1122
+ {
1123
+ "name": "hotarumouth_xl_circle2_v20.safetensors",
1124
+ "path": "sdxl/hotarumouth_xl_circle2_v20.safetensors",
1125
+ "title": "丸い口",
1126
+ "doc": "sdxl/circlemouth/README.md",
1127
+ "image": "sdxl/circlemouth/hotarumouth_xl_circle2_v20_1.webp",
1128
+ "group_sample": false,
1129
+ "size": 21581260
1130
+ },
1131
+ {
1132
+ "name": "hotarumouth_xl_small_v10.safetensors",
1133
+ "path": "sdxl/hotarumouth_xl_small_v10.safetensors",
1134
+ "title": "丸い口",
1135
+ "doc": "sdxl/circlemouth/README.md",
1136
+ "image": "sdxl/circlemouth/hotarumouth_xl_small_v10.webp",
1137
+ "group_sample": false,
1138
+ "size": 93012288
1139
+ },
1140
+ {
1141
+ "name": "erase_human_ear_v10_ill.safetensors",
1142
+ "path": "sdxl/erase_human_ear_v10_ill.safetensors",
1143
+ "title": "人耳消去(ケモ耳用)",
1144
+ "doc": "sdxl/human_ear_eraser/README.md",
1145
+ "image": "sdxl/human_ear_eraser/thumb.webp",
1146
+ "group_sample": true,
1147
+ "size": 42878828
1148
+ },
1149
+ {
1150
+ "name": "worriedeyes_xl_v10.safetensors",
1151
+ "path": "sdxl/worriedeyes_xl_v10.safetensors",
1152
+ "title": "伏し目",
1153
+ "doc": "sdxl/worriedeyes/README.md",
1154
+ "image": "sdxl/worriedeyes/worriedeyes_xl_v10_01.webp",
1155
+ "group_sample": false,
1156
+ "size": 85438284
1157
+ },
1158
+ {
1159
+ "name": "facelightup_xl_v20.safetensors",
1160
+ "path": "sdxl/facelightup_xl_v20.safetensors",
1161
+ "title": "前面ライトアップ",
1162
+ "doc": "sdxl/facelightup/README.md",
1163
+ "image": "sdxl/facelightup/thumb.webp",
1164
+ "group_sample": true,
1165
+ "size": 23496456
1166
+ },
1167
+ {
1168
+ "name": "hotarueye_xl_tsurime1_v20.safetensors",
1169
+ "path": "sdxl/hotarueye_xl_tsurime1_v20.safetensors",
1170
+ "title": "吊り目",
1171
+ "doc": "sdxl/tsurime/README.md",
1172
+ "image": "sdxl/tsurime/hotarueye_xl_tsurime1_v20_1_t.webp",
1173
+ "group_sample": false,
1174
+ "size": 42861100
1175
+ },
1176
+ {
1177
+ "name": "hotarueye_xl_tsurime2_v20.safetensors",
1178
+ "path": "sdxl/hotarueye_xl_tsurime2_v20.safetensors",
1179
+ "title": "吊り目",
1180
+ "doc": "sdxl/tsurime/README.md",
1181
+ "image": "sdxl/tsurime/hotarueye_xl_tsurime2_v20_1_t.webp",
1182
+ "group_sample": false,
1183
+ "size": 42861100
1184
+ },
1185
+ {
1186
+ "name": "hotarueye_xl_tsurime3_v20.safetensors",
1187
+ "path": "sdxl/hotarueye_xl_tsurime3_v20.safetensors",
1188
+ "title": "吊り目",
1189
+ "doc": "sdxl/tsurime/README.md",
1190
+ "image": "sdxl/tsurime/hotarueye_xl_tsurime3_v20_1_t.webp",
1191
+ "group_sample": false,
1192
+ "size": 42861100
1193
+ },
1194
+ {
1195
+ "name": "hotarueye_xl_tsurime4_v20.safetensors",
1196
+ "path": "sdxl/hotarueye_xl_tsurime4_v20.safetensors",
1197
+ "title": "吊り目",
1198
+ "doc": "sdxl/tsurime/README.md",
1199
+ "image": "sdxl/tsurime/hotarueye_xl_tsurime4_v20_1_t.webp",
1200
+ "group_sample": false,
1201
+ "size": 42861100
1202
+ },
1203
+ {
1204
+ "name": "wideclosedmouth1_xl_v20.safetensors",
1205
+ "path": "sdxl/wideclosedmouth1_xl_v20.safetensors",
1206
+ "title": "大きな口",
1207
+ "doc": "sdxl/widemouth/README.md",
1208
+ "image": "sdxl/widemouth/wideclosedmouth1_xl_v20_1.webp",
1209
+ "group_sample": false,
1210
+ "size": 21581260
1211
+ },
1212
+ {
1213
+ "name": "wideclosedmouth2_xl_v20.safetensors",
1214
+ "path": "sdxl/wideclosedmouth2_xl_v20.safetensors",
1215
+ "title": "大きな口",
1216
+ "doc": "sdxl/widemouth/README.md",
1217
+ "image": "sdxl/widemouth/wideclosedmouth2_xl_v20_1.webp",
1218
+ "group_sample": false,
1219
+ "size": 21581260
1220
+ },
1221
+ {
1222
+ "name": "widemouth1_xl_v20.safetensors",
1223
+ "path": "sdxl/widemouth1_xl_v20.safetensors",
1224
+ "title": "大きな口",
1225
+ "doc": "sdxl/widemouth/README.md",
1226
+ "image": "sdxl/widemouth/widemouth1_xl_v20_1.webp",
1227
+ "group_sample": false,
1228
+ "size": 21581260
1229
+ },
1230
+ {
1231
+ "name": "widemouth2_xl_v20.safetensors",
1232
+ "path": "sdxl/widemouth2_xl_v20.safetensors",
1233
+ "title": "大きな口",
1234
+ "doc": "sdxl/widemouth/README.md",
1235
+ "image": "sdxl/widemouth/widemouth2_xl_v20_1.webp",
1236
+ "group_sample": false,
1237
+ "size": 21581260
1238
+ },
1239
+ {
1240
+ "name": "sensualface1_xl_v30.safetensors",
1241
+ "path": "sdxl/sensualface1_xl_v30.safetensors",
1242
+ "title": "官能的な表情",
1243
+ "doc": "sdxl/sensualface/README.md",
1244
+ "image": "sdxl/sensualface/sensualface1_xl_v30_01.webp",
1245
+ "group_sample": false,
1246
+ "size": 42861100
1247
+ },
1248
+ {
1249
+ "name": "sensualface2_xl_v30.safetensors",
1250
+ "path": "sdxl/sensualface2_xl_v30.safetensors",
1251
+ "title": "官能的な表情",
1252
+ "doc": "sdxl/sensualface/README.md",
1253
+ "image": "sdxl/sensualface/sensualface2_xl_v30_01.webp",
1254
+ "group_sample": false,
1255
+ "size": 42861100
1256
+ },
1257
+ {
1258
+ "name": "smallclosedmouth1_xl_v20.safetensors",
1259
+ "path": "sdxl/smallclosedmouth1_xl_v20.safetensors",
1260
+ "title": "小さく閉じた口",
1261
+ "doc": "sdxl/smallclosedmouth/README.md",
1262
+ "image": "sdxl/smallclosedmouth/smallclosedmouth1_xl_v20_1.webp",
1263
+ "group_sample": false,
1264
+ "size": 21581260
1265
+ },
1266
+ {
1267
+ "name": "smallclosedmouth2_xl_v20.safetensors",
1268
+ "path": "sdxl/smallclosedmouth2_xl_v20.safetensors",
1269
+ "title": "小さく閉じた口",
1270
+ "doc": "sdxl/smallclosedmouth/README.md",
1271
+ "image": "sdxl/smallclosedmouth/smallclosedmouth2_xl_v20_1.webp",
1272
+ "group_sample": false,
1273
+ "size": 21581260
1274
+ },
1275
+ {
1276
+ "name": "smugface_xl_v21.safetensors",
1277
+ "path": "sdxl/smugface_xl_v21.safetensors",
1278
+ "title": "小悪魔の笑み",
1279
+ "doc": "sdxl/smugface/README.md",
1280
+ "image": "sdxl/smugface/thumb.webp",
1281
+ "group_sample": true,
1282
+ "size": 43808420
1283
+ },
1284
+ {
1285
+ "name": "darkness_xl_v20.safetensors",
1286
+ "path": "sdxl/darkness_xl_v20.safetensors",
1287
+ "title": "暗闇化",
1288
+ "doc": "sdxl/darkness/README.md",
1289
+ "image": "sdxl/darkness/darkness_xl_v20_01.webp",
1290
+ "group_sample": false,
1291
+ "size": 46667768
1292
+ },
1293
+ {
1294
+ "name": "hotarumouth_xl_chestnut_v20.safetensors",
1295
+ "path": "sdxl/hotarumouth_xl_chestnut_v20.safetensors",
1296
+ "title": "栗型の口",
1297
+ "doc": "sdxl/chestnutmouth/README.md",
1298
+ "image": "sdxl/chestnutmouth/hotarumouth_xl_chestnut_v20_1.webp",
1299
+ "group_sample": false,
1300
+ "size": 21581260
1301
+ },
1302
+ {
1303
+ "name": "clenchedteeth1_xl_v10.safetensors",
1304
+ "path": "sdxl/clenchedteeth1_xl_v10.safetensors",
1305
+ "title": "歯を食いしばった口",
1306
+ "doc": "sdxl/clenchedteeth/README.md",
1307
+ "image": "sdxl/clenchedteeth/clenchedteeth1_xl_v10.webp",
1308
+ "group_sample": false,
1309
+ "size": 21581260
1310
+ },
1311
+ {
1312
+ "name": "clenchedteeth2_xl_v10.safetensors",
1313
+ "path": "sdxl/clenchedteeth2_xl_v10.safetensors",
1314
+ "title": "歯を食いしばった口",
1315
+ "doc": "sdxl/clenchedteeth/README.md",
1316
+ "image": "sdxl/clenchedteeth/clenchedteeth2_xl_v10.webp",
1317
+ "group_sample": false,
1318
+ "size": 21581260
1319
+ },
1320
+ {
1321
+ "name": "clenchedteeth3_xl_v10.safetensors",
1322
+ "path": "sdxl/clenchedteeth3_xl_v10.safetensors",
1323
+ "title": "歯を食いしばった口",
1324
+ "doc": "sdxl/clenchedteeth/README.md",
1325
+ "image": "sdxl/clenchedteeth/clenchedteeth3_xl_v10.webp",
1326
+ "group_sample": false,
1327
+ "size": 21581260
1328
+ },
1329
+ {
1330
+ "name": "gekioko_xl_v20.safetensors",
1331
+ "path": "sdxl/gekioko_xl_v20.safetensors",
1332
+ "title": "激おこ顔",
1333
+ "doc": "sdxl/gekioko/README.md",
1334
+ "image": "sdxl/gekioko/gekioko_xl_v20_00.webp",
1335
+ "group_sample": false,
1336
+ "size": 43808420
1337
+ },
1338
+ {
1339
+ "name": "whiteeyelash_xl_v20_ill.safetensors",
1340
+ "path": "sdxl/whiteeyelash_xl_v20_ill.safetensors",
1341
+ "title": "白い睫毛補助",
1342
+ "doc": "sdxl/whiteeyelash/README.md",
1343
+ "image": "sdxl/whiteeyelash/whiteeyelash_xl_v20_ill_1.webp",
1344
+ "group_sample": false,
1345
+ "size": 21581260
1346
+ },
1347
+ {
1348
+ "name": "mekakure_left_xl_v20.safetensors",
1349
+ "path": "sdxl/mekakure_left_xl_v20.safetensors",
1350
+ "title": "目隠れ補助",
1351
+ "doc": "sdxl/mekakure/README.md",
1352
+ "image": "sdxl/mekakure/mekakure_left_xl_v20_01.webp",
1353
+ "group_sample": false,
1354
+ "size": 42861124
1355
+ },
1356
+ {
1357
+ "name": "mekakure_right_xl_v20.safetensors",
1358
+ "path": "sdxl/mekakure_right_xl_v20.safetensors",
1359
+ "title": "目隠れ補助",
1360
+ "doc": "sdxl/mekakure/README.md",
1361
+ "image": "sdxl/mekakure/mekakure_right_xl_v20_01.webp",
1362
+ "group_sample": false,
1363
+ "size": 43808444
1364
+ },
1365
+ {
1366
+ "name": "mekakure_xl_v20.safetensors",
1367
+ "path": "sdxl/mekakure_xl_v20.safetensors",
1368
+ "title": "目隠れ補助",
1369
+ "doc": "sdxl/mekakure/README.md",
1370
+ "image": "sdxl/mekakure/mekakure_xl_v20_01.webp",
1371
+ "group_sample": false,
1372
+ "size": 87304948
1373
+ },
1374
+ {
1375
+ "name": "hotarueye_xl_sleepy_v20.safetensors",
1376
+ "path": "sdxl/hotarueye_xl_sleepy_v20.safetensors",
1377
+ "title": "眠そうな目",
1378
+ "doc": "sdxl/sleepy/README.md",
1379
+ "image": "sdxl/sleepy/hotarueye_xl_sleepy_v20_1.webp",
1380
+ "group_sample": false,
1381
+ "size": 42861100
1382
+ },
1383
+ {
1384
+ "name": "eyelashext_v10.safetensors",
1385
+ "path": "sdxl/eyelashext_v10.safetensors",
1386
+ "title": "睫毛増減",
1387
+ "doc": "sdxl/eyelashext/README.md",
1388
+ "image": "sdxl/eyelashext/thumb.webp",
1389
+ "group_sample": true,
1390
+ "size": 21598988
1391
+ },
1392
+ {
1393
+ "name": "hotarupupil_xl_type1_v10.safetensors",
1394
+ "path": "sdxl/hotarupupil_xl_type1_v10.safetensors",
1395
+ "title": "瞳コレクション",
1396
+ "doc": "sdxl/pupil/README.md",
1397
+ "image": "sdxl/pupil/v10_sample1.webp",
1398
+ "group_sample": true,
1399
+ "size": 21581260,
1400
+ "sample_viewport": {
1401
+ "x": 768,
1402
+ "y": 0,
1403
+ "width": 400,
1404
+ "height": 400,
1405
+ "source_width": 1968,
1406
+ "source_height": 800
1407
+ }
1408
+ },
1409
+ {
1410
+ "name": "hotarupupil_xl_type2_v10.safetensors",
1411
+ "path": "sdxl/hotarupupil_xl_type2_v10.safetensors",
1412
+ "title": "瞳コレクション",
1413
+ "doc": "sdxl/pupil/README.md",
1414
+ "image": "sdxl/pupil/v10_sample1.webp",
1415
+ "group_sample": true,
1416
+ "size": 21581260,
1417
+ "sample_viewport": {
1418
+ "x": 1168,
1419
+ "y": 0,
1420
+ "width": 400,
1421
+ "height": 400,
1422
+ "source_width": 1968,
1423
+ "source_height": 800
1424
+ }
1425
+ },
1426
+ {
1427
+ "name": "hotarupupil_xl_type3_v10.safetensors",
1428
+ "path": "sdxl/hotarupupil_xl_type3_v10.safetensors",
1429
+ "title": "瞳コレクション",
1430
+ "doc": "sdxl/pupil/README.md",
1431
+ "image": "sdxl/pupil/v10_sample1.webp",
1432
+ "group_sample": true,
1433
+ "size": 21581260,
1434
+ "sample_viewport": {
1435
+ "x": 1568,
1436
+ "y": 0,
1437
+ "width": 400,
1438
+ "height": 400,
1439
+ "source_width": 1968,
1440
+ "source_height": 800
1441
+ }
1442
+ },
1443
+ {
1444
+ "name": "hotarupupil_xl_type4_v10.safetensors",
1445
+ "path": "sdxl/hotarupupil_xl_type4_v10.safetensors",
1446
+ "title": "瞳コレクション",
1447
+ "doc": "sdxl/pupil/README.md",
1448
+ "image": "sdxl/pupil/v10_sample1.webp",
1449
+ "group_sample": true,
1450
+ "size": 21581260,
1451
+ "sample_viewport": {
1452
+ "x": 768,
1453
+ "y": 400,
1454
+ "width": 400,
1455
+ "height": 400,
1456
+ "source_width": 1968,
1457
+ "source_height": 800
1458
+ }
1459
+ },
1460
+ {
1461
+ "name": "hotarupupil_xl_type5_v10.safetensors",
1462
+ "path": "sdxl/hotarupupil_xl_type5_v10.safetensors",
1463
+ "title": "瞳コレクション",
1464
+ "doc": "sdxl/pupil/README.md",
1465
+ "image": "sdxl/pupil/v10_sample1.webp",
1466
+ "group_sample": true,
1467
+ "size": 21581260,
1468
+ "sample_viewport": {
1469
+ "x": 1168,
1470
+ "y": 400,
1471
+ "width": 400,
1472
+ "height": 400,
1473
+ "source_width": 1968,
1474
+ "source_height": 800
1475
+ }
1476
+ },
1477
+ {
1478
+ "name": "hotarupupil_xl_type6_v10.safetensors",
1479
+ "path": "sdxl/hotarupupil_xl_type6_v10.safetensors",
1480
+ "title": "瞳コレクション",
1481
+ "doc": "sdxl/pupil/README.md",
1482
+ "image": "sdxl/pupil/v10_sample1.webp",
1483
+ "group_sample": true,
1484
+ "size": 21581260,
1485
+ "sample_viewport": {
1486
+ "x": 1568,
1487
+ "y": 400,
1488
+ "width": 400,
1489
+ "height": 400,
1490
+ "source_width": 1968,
1491
+ "source_height": 800
1492
+ }
1493
+ },
1494
+ {
1495
+ "name": "hotarueye_xl_hosome1_v30.safetensors",
1496
+ "path": "sdxl/hotarueye_xl_hosome1_v30.safetensors",
1497
+ "title": "細目補助",
1498
+ "doc": "sdxl/hosome/README.md",
1499
+ "image": "sdxl/hosome/type1_v30.webp",
1500
+ "group_sample": false,
1501
+ "size": 42878828
1502
+ },
1503
+ {
1504
+ "name": "hotarueye_xl_hosome2_v30.safetensors",
1505
+ "path": "sdxl/hotarueye_xl_hosome2_v30.safetensors",
1506
+ "title": "細目補助",
1507
+ "doc": "sdxl/hosome/README.md",
1508
+ "image": "sdxl/hosome/type2_v30.webp",
1509
+ "group_sample": false,
1510
+ "size": 42878828
1511
+ },
1512
+ {
1513
+ "name": "sneerface_xl_v20.safetensors",
1514
+ "path": "sdxl/sneerface_xl_v20.safetensors",
1515
+ "title": "蔑みの表情",
1516
+ "doc": "sdxl/sneerface/README.md",
1517
+ "image": "sdxl/sneerface/sneerface_xl_v20_sample.webp",
1518
+ "group_sample": false,
1519
+ "size": 42861100
1520
+ },
1521
+ {
1522
+ "name": "yudedako_xl_type1_v20.safetensors",
1523
+ "path": "sdxl/yudedako_xl_type1_v20.safetensors",
1524
+ "title": "赤面(茹でダコ顔)",
1525
+ "doc": "sdxl/yudedako/README.md",
1526
+ "image": "sdxl/yudedako/yudedako_xl_type1_v20_1.webp",
1527
+ "group_sample": false,
1528
+ "size": 21581260
1529
+ },
1530
+ {
1531
+ "name": "yudedako_xl_type2_v20.safetensors",
1532
+ "path": "sdxl/yudedako_xl_type2_v20.safetensors",
1533
+ "title": "赤面(茹でダコ顔)",
1534
+ "doc": "sdxl/yudedako/README.md",
1535
+ "image": "sdxl/yudedako/yudedako_xl_type2_v20_1.webp",
1536
+ "group_sample": false,
1537
+ "size": 21581260
1538
+ },
1539
+ {
1540
+ "name": "yudedako_xl_type3_v20.safetensors",
1541
+ "path": "sdxl/yudedako_xl_type3_v20.safetensors",
1542
+ "title": "赤面(茹でダコ顔)",
1543
+ "doc": "sdxl/yudedako/README.md",
1544
+ "image": "sdxl/yudedako/yudedako_xl_type3_v20_1.webp",
1545
+ "group_sample": false,
1546
+ "size": 21581260
1547
+ },
1548
+ {
1549
+ "name": "hotarueye_xl_closed1_v20.safetensors",
1550
+ "path": "sdxl/hotarueye_xl_closed1_v20.safetensors",
1551
+ "title": "閉じ目補助",
1552
+ "doc": "sdxl/closedeye/README.md",
1553
+ "image": "sdxl/closedeye/hotarueye_xl_closed1_v20_01.webp",
1554
+ "group_sample": false,
1555
+ "size": 42877584
1556
+ },
1557
+ {
1558
+ "name": "hotarueye_xl_closed2_v20.safetensors",
1559
+ "path": "sdxl/hotarueye_xl_closed2_v20.safetensors",
1560
+ "title": "閉じ目補助",
1561
+ "doc": "sdxl/closedeye/README.md",
1562
+ "image": "sdxl/closedeye/hotarueye_xl_closed2_v20_01.webp",
1563
+ "group_sample": false,
1564
+ "size": 42877584
1565
+ },
1566
+ {
1567
+ "name": "hotarueye_xl_closed3_v20.safetensors",
1568
+ "path": "sdxl/hotarueye_xl_closed3_v20.safetensors",
1569
+ "title": "閉じ目補助",
1570
+ "doc": "sdxl/closedeye/README.md",
1571
+ "image": "sdxl/closedeye/hotarueye_xl_closed3_v20_01.webp",
1572
+ "group_sample": false,
1573
+ "size": 42878828
1574
+ },
1575
+ {
1576
+ "name": "paleface_xl_v20.safetensors",
1577
+ "path": "sdxl/paleface_xl_v20.safetensors",
1578
+ "title": "青ざめた顔",
1579
+ "doc": "sdxl/paleface/README.md",
1580
+ "image": "sdxl/paleface/paleface_xl_v20_01.webp",
1581
+ "group_sample": false,
1582
+ "size": 21581260
1583
+ },
1584
+ {
1585
+ "name": "hotarueye_xl_surprised_v30.safetensors",
1586
+ "path": "sdxl/hotarueye_xl_surprised_v30.safetensors",
1587
+ "title": "驚いた目",
1588
+ "doc": "sdxl/surprised/README.md",
1589
+ "image": "sdxl/surprised/hotarueye_xl_surprised_v30_1.webp",
1590
+ "group_sample": false,
1591
+ "size": 21581260
1592
+ },
1593
+ {
1594
+ "name": "hotarueye_xl_comicjitome01_v10.safetensors",
1595
+ "path": "sdxl/hotarueye_xl_comicjitome01_v10.safetensors",
1596
+ "title": "ジト目(デフォルメ型) Type1",
1597
+ "doc": "sdxl/README.md",
1598
+ "image": "sdxl/hotarueye_xl_comicjitome01_v10.webp",
1599
+ "group_sample": false,
1600
+ "size": 93012288,
1601
+ "pony_only": true
1602
+ },
1603
+ {
1604
+ "name": "hotarueye_xl_comicjitome02_v10.safetensors",
1605
+ "path": "sdxl/hotarueye_xl_comicjitome02_v10.safetensors",
1606
+ "title": "ジト目(デフォルメ型) Type2",
1607
+ "doc": "sdxl/README.md",
1608
+ "image": "sdxl/hotarueye_xl_comicjitome02_v10.webp",
1609
+ "group_sample": false,
1610
+ "size": 93012288,
1611
+ "pony_only": true
1612
+ },
1613
+ {
1614
+ "name": "hotarueye_xl_comicjitome03_v10.safetensors",
1615
+ "path": "sdxl/hotarueye_xl_comicjitome03_v10.safetensors",
1616
+ "title": "ジト目(デフォルメ型) Type3",
1617
+ "doc": "sdxl/README.md",
1618
+ "image": "sdxl/hotarueye_xl_comicjitome03_v10.webp",
1619
+ "group_sample": false,
1620
+ "size": 93012288,
1621
+ "pony_only": true
1622
+ },
1623
+ {
1624
+ "name": "hotarueye_xl_comicjitome04_v10.safetensors",
1625
+ "path": "sdxl/hotarueye_xl_comicjitome04_v10.safetensors",
1626
+ "title": "ジト目(デフォルメ型) Type4",
1627
+ "doc": "sdxl/README.md",
1628
+ "image": "sdxl/hotarueye_xl_comicjitome04_v10.webp",
1629
+ "group_sample": false,
1630
+ "size": 93012288,
1631
+ "pony_only": true
1632
+ },
1633
+ {
1634
+ "name": "hotarueye_xl_comicjitome05_v10.safetensors",
1635
+ "path": "sdxl/hotarueye_xl_comicjitome05_v10.safetensors",
1636
+ "title": "ジト目(デフォルメ型) Type5",
1637
+ "doc": "sdxl/README.md",
1638
+ "image": "sdxl/hotarueye_xl_comicjitome05_v10.webp",
1639
+ "group_sample": false,
1640
+ "size": 93012288,
1641
+ "pony_only": true
1642
+ },
1643
+ {
1644
+ "name": "hotarueye_xl_comicjitome06_v10.safetensors",
1645
+ "path": "sdxl/hotarueye_xl_comicjitome06_v10.safetensors",
1646
+ "title": "ジト目(デフォルメ型) Type6",
1647
+ "doc": "sdxl/README.md",
1648
+ "image": "sdxl/hotarueye_xl_comicjitome06_v10.webp",
1649
+ "group_sample": false,
1650
+ "size": 93012288,
1651
+ "pony_only": true
1652
+ },
1653
+ {
1654
+ "name": "hotarueye_xl_comicjitome07_v10.safetensors",
1655
+ "path": "sdxl/hotarueye_xl_comicjitome07_v10.safetensors",
1656
+ "title": "ジト目(デフォルメ型) Type7",
1657
+ "doc": "sdxl/README.md",
1658
+ "image": "sdxl/hotarueye_xl_comicjitome07_v10.webp",
1659
+ "group_sample": false,
1660
+ "size": 98705896,
1661
+ "pony_only": true
1662
+ },
1663
+ {
1664
+ "name": "erase_human_ear_v10_pony.safetensors",
1665
+ "path": "sdxl/erase_human_ear_v10_pony.safetensors",
1666
+ "title": "ダウンロード",
1667
+ "doc": "sdxl/human_ear_eraser/README.md",
1668
+ "image": "sdxl/human_ear_eraser/thumb.webp",
1669
+ "group_sample": true,
1670
+ "size": 42878828,
1671
+ "pony_only": true
1672
+ },
1673
+ {
1674
+ "name": "hotarueye_xl_comic01_v20_pony.safetensors",
1675
+ "path": "sdxl/hotarueye_xl_comic01_v20_pony.safetensors",
1676
+ "title": "デフォル眼 Type1",
1677
+ "doc": "sdxl/README.md",
1678
+ "image": "sdxl/hotarueye_xl_comic01_v20.webp",
1679
+ "group_sample": false,
1680
+ "size": 85438260,
1681
+ "pony_only": true
1682
+ },
1683
+ {
1684
+ "name": "hotarueye_xl_comic02_v10.safetensors",
1685
+ "path": "sdxl/hotarueye_xl_comic02_v10.safetensors",
1686
+ "title": "デフォル眼 Type2",
1687
+ "doc": "sdxl/README.md",
1688
+ "image": "sdxl/hotarueye_xl_comic02_v10.webp",
1689
+ "group_sample": false,
1690
+ "size": 93012288,
1691
+ "pony_only": true
1692
+ },
1693
+ {
1694
+ "name": "hotarueye_xl_comic03_v10.safetensors",
1695
+ "path": "sdxl/hotarueye_xl_comic03_v10.safetensors",
1696
+ "title": "デフォル眼 Type3",
1697
+ "doc": "sdxl/README.md",
1698
+ "image": "sdxl/hotarueye_xl_comic03_v10.webp",
1699
+ "group_sample": false,
1700
+ "size": 93012288,
1701
+ "pony_only": true
1702
+ },
1703
+ {
1704
+ "name": "hotarueye_xl_comic04_v10_pony.safetensors",
1705
+ "path": "sdxl/hotarueye_xl_comic04_v10_pony.safetensors",
1706
+ "title": "デフォル眼 Type4",
1707
+ "doc": "sdxl/README.md",
1708
+ "image": "sdxl/hotarueye_xl_comic04_v10.webp",
1709
+ "group_sample": false,
1710
+ "size": 42878796,
1711
+ "pony_only": true
1712
+ },
1713
+ {
1714
+ "name": "hotarueye_xl_white01_v10.safetensors",
1715
+ "path": "sdxl/hotarueye_xl_white01_v10.safetensors",
1716
+ "title": "白目1",
1717
+ "doc": "sdxl/README.md",
1718
+ "image": "sdxl/hotarueye_xl_white01_v10.webp",
1719
+ "group_sample": false,
1720
+ "size": 93012288,
1721
+ "pony_only": true
1722
+ },
1723
+ {
1724
+ "name": "hotarueye_xl_white03_v10.safetensors",
1725
+ "path": "sdxl/hotarueye_xl_white03_v10.safetensors",
1726
+ "title": "白目3",
1727
+ "doc": "sdxl/README.md",
1728
+ "image": "sdxl/hotarueye_xl_white03_v10.webp",
1729
+ "group_sample": false,
1730
+ "size": 93012288,
1731
+ "pony_only": true
1732
+ },
1733
+ {
1734
+ "name": "hotarueye_xl_white04_v10.safetensors",
1735
+ "path": "sdxl/hotarueye_xl_white04_v10.safetensors",
1736
+ "title": "白目4",
1737
+ "doc": "sdxl/README.md",
1738
+ "image": "sdxl/hotarueye_xl_white04_v10.webp",
1739
+ "group_sample": false,
1740
+ "size": 93012288,
1741
+ "pony_only": true
1742
+ },
1743
+ {
1744
+ "name": "hotarueye_xl_black01_v10.safetensors",
1745
+ "path": "sdxl/hotarueye_xl_black01_v10.safetensors",
1746
+ "title": "黒目1",
1747
+ "doc": "sdxl/README.md",
1748
+ "image": "sdxl/hotarueye_xl_black01_v10.webp",
1749
+ "group_sample": false,
1750
+ "size": 93012288,
1751
+ "pony_only": true
1752
+ },
1753
+ {
1754
+ "name": "hotarueye_xl_black02_v10.safetensors",
1755
+ "path": "sdxl/hotarueye_xl_black02_v10.safetensors",
1756
+ "title": "黒目2",
1757
+ "doc": "sdxl/README.md",
1758
+ "image": "sdxl/hotarueye_xl_black02_v10.webp",
1759
+ "group_sample": false,
1760
+ "size": 93012288,
1761
+ "pony_only": true
1762
+ }
1763
+ ],
1764
+ "pony_paths": [
1765
+ "sdxl/erase_human_ear_v10_pony.safetensors",
1766
+ "sdxl/hotarueye_xl_black01_v10.safetensors",
1767
+ "sdxl/hotarueye_xl_black02_v10.safetensors",
1768
+ "sdxl/hotarueye_xl_comic01_v20_pony.safetensors",
1769
+ "sdxl/hotarueye_xl_comic02_v10.safetensors",
1770
+ "sdxl/hotarueye_xl_comic03_v10.safetensors",
1771
+ "sdxl/hotarueye_xl_comic04_v10_pony.safetensors",
1772
+ "sdxl/hotarueye_xl_comicjitome01_v10.safetensors",
1773
+ "sdxl/hotarueye_xl_comicjitome02_v10.safetensors",
1774
+ "sdxl/hotarueye_xl_comicjitome03_v10.safetensors",
1775
+ "sdxl/hotarueye_xl_comicjitome04_v10.safetensors",
1776
+ "sdxl/hotarueye_xl_comicjitome05_v10.safetensors",
1777
+ "sdxl/hotarueye_xl_comicjitome06_v10.safetensors",
1778
+ "sdxl/hotarueye_xl_comicjitome07_v10.safetensors",
1779
+ "sdxl/hotarueye_xl_white01_v10.safetensors",
1780
+ "sdxl/hotarueye_xl_white03_v10.safetensors",
1781
+ "sdxl/hotarueye_xl_white04_v10.safetensors"
1782
+ ]
1783
+ }
assets/settings/settings.css ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .bs-settings { padding: 12px; border: 1px solid var(--border-color-primary, #aaa); border-radius: 8px; }
2
+ .bs-settings summary { cursor: pointer; font-weight: 600; }
3
+ .bs-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin-top: 8px; }
4
+ .bs-controls label { display: flex; align-items: center; gap: 6px; }
5
+ .bs-controls button { border: 1px solid var(--border-color-primary, #aaa); border-radius: 6px; padding: 6px 10px; cursor: pointer; }
6
+ .bs-settings p { font-size: 0.85em; margin: 7px 0 0; }
7
+ .bs-message { opacity: 0.85; }
8
+ @media (max-width: 600px) { .bs-controls label { width: 100%; } }
9
+ .bf-panel { margin-top: 12px; padding: 12px; border: 1px solid var(--border-color-primary, #aaa); border-radius: 8px; }
10
+ .bf-panel summary { cursor: pointer; font-weight: 600; }
11
+ .bf-panel p { font-size: .85em; margin-top: 8px; }
12
+ .bf-panel select, .bf-panel input:not([type=file]) { max-width: 100%; min-width: 0; padding: 6px; border: 1px solid var(--border-color-primary, #aaa); border-radius: 6px; background: var(--input-background-fill, white); color: var(--body-text-color, black); }
13
+ .bf-panel .bs-controls label { flex-wrap: wrap; }
14
+ .bf-panel button:disabled { opacity: .5; cursor: default; }
15
+ @media(max-width:600px) { .bf-panel .bs-controls label { width: 100%; } .bf-panel select, .bf-panel input:not([type=file]) { width: 100%; } }
assets/settings/settings.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <details class="bs-settings">
2
+ <summary>設定の保存・初期化(このブラウザ)</summary>
3
+ <div class="bs-controls">
4
+ <label><input class="bs-enabled" type="checkbox" checked> 設定をこのブラウザに保存</label>
5
+ <label><input class="bs-prompts" type="checkbox" checked> プロンプトも保存</label>
6
+ <button class="bs-delete" type="button">保存設定を削除</button>
7
+ <button class="bs-reset" type="button">初期設定に戻す</button>
8
+ </div>
9
+ <p>このモードのプロンプトを含む設定を保存します。共有端末では保存をOFFにしてください。認証情報・画像は保存しません。</p>
10
+ <p>設定・お気に入りは、このブラウザのサイトデータに保存されます。Cookie・サイトデータの削除やブラウザの初期化で失われます。画像などのキャッシュだけの削除では通常は消えません。別の端末・ブラウザには自動同期されません。</p>
11
+ <p class="bs-message" role="status" aria-live="polite">保存設定を確認中…</p>
12
+ </details>
13
+ <details class="bf-panel">
14
+ <summary>お気に入り(このブラウザ・モード別)</summary>
15
+ <p>名前を付けて各モード最大100件まで保存できます。追加・上書きは自動保存のON/OFFにかかわらずプロンプトも保存します。画像・認証情報は保存しません。</p>
16
+ <div class="bs-controls">
17
+ <label>名前で検索<input class="bf-search" type="search" maxlength="80" placeholder="名前の一部を入力" aria-label="お気に入り名を検索"></label>
18
+ </div>
19
+ <p class="bf-count" role="status" aria-live="polite"></p>
20
+ <div class="bs-controls">
21
+ <label>お気に入り<select class="bf-list" aria-label="お気に入り一覧"></select></label>
22
+ <label>名前<input class="bf-name" maxlength="80" placeholder="例:水彩・正方形" aria-label="お気に入りの名前"></label>
23
+ <label>保存するSeed<select class="bf-seed"><option value="input">現在の入力Seed(-1ならランダム)</option><option value="last">直前の生成で実際に使ったSeed</option></select></label>
24
+ </div>
25
+ <div class="bs-controls">
26
+ <button type="button" class="bf-add">お気に入りに追加</button>
27
+ <button type="button" class="bf-apply">設定を読み込む</button>
28
+ <button type="button" class="bf-update">現在の設定で上書き</button>
29
+ <button type="button" class="bf-delete">選択したお気に入りを削除</button>
30
+ <button type="button" class="bf-export">エクスポート</button>
31
+ <button type="button" class="bf-import">インポート</button>
32
+ <input class="bf-file" type="file" accept=".json,application/json" hidden>
33
+ <button type="button" class="bf-clear">全件クリア</button>
34
+ </div>
35
+ <p>「設定を読み込む」は、選択したお気に入りの設定を入力欄に反映します。画像生成は開始しません。</p>
36
+ <p>サイトデータの削除やブラウザの初期化でお気に入りは失われます。大切なお気に入りはJSONでエクスポートしてください。別の端末・ブラウザには自動同期されません。シークレットモードでは通常、終了時に消えます。</p>
37
+ <p>別端末への移行はJSONの入出力をご利用ください。JSONにはプロンプトが含まれます。共有端末やファイルの共有にはご注意ください。全件クリアは現在の入力欄・前回設定の保存には影響しません。</p>
38
+ <p class="bf-message" role="status" aria-live="polite"></p>
39
+ </details>
assets/settings/settings.js ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const key = `image-model-collection:settings:v1:${props.family}`;
2
+ const enabled = element.querySelector(".bs-enabled");
3
+ const prompts = element.querySelector(".bs-prompts");
4
+ const message = element.querySelector(".bs-message");
5
+ let values = {}, ready = false, timer;
6
+ function note(text) { message.textContent = text; }
7
+ function write() {
8
+ clearTimeout(timer);
9
+ if (!ready) return;
10
+ const filtered = {};
11
+ for (const name of props.field_names) {
12
+ if (!prompts.checked && (name === "prompt" || name === "negative_prompt")) continue;
13
+ if (Object.hasOwn(values, name)) filtered[name] = values[name];
14
+ }
15
+ try {
16
+ // The OFF marker remembers consent only, not generation settings.
17
+ const record = enabled.checked
18
+ ? {version:1, family:props.family, enabled:true, prompts:prompts.checked, values:filtered}
19
+ : {version:1, family:props.family, enabled:false};
20
+ localStorage.setItem(key, JSON.stringify(record));
21
+ note(enabled.checked ? "設定を保存しました(このブラウザのみ)。" : "設定保存はOFFです。保存済みの設定は削除しました。");
22
+ } catch (_) { note("ブラウザの保存領域を利用できません。現在の設定で生成はできます。"); }
23
+ }
24
+ function send(action, record) {
25
+ props.value = {action, record};
26
+ trigger("submit");
27
+ }
28
+ watch("value", () => {
29
+ const data = props.value;
30
+ if (data?.action === "favorite_saved") { completeFavorite(data); return; }
31
+ if (!data || !["sync", "ready"].includes(data.action)) return;
32
+ values = data.values || {};
33
+ if (data.action === "ready") {
34
+ ready = true;
35
+ enabled.disabled = false; prompts.disabled = false;
36
+ element.querySelector(".bs-reset").disabled = false;
37
+ favoriteBusy = false; renderFavorites();
38
+ if (data.favorite_applied) favoriteNote((data.warning || "お気に入りの設定を読み込みました。画像生成は開始していません。") + ` 使用するSeed:${values.seed}(-1はランダム)`);
39
+ }
40
+ clearTimeout(timer);
41
+ timer = setTimeout(write, 500);
42
+ });
43
+ enabled.addEventListener("change", write);
44
+ prompts.addEventListener("change", write);
45
+ element.querySelector(".bs-delete").addEventListener("click", () => {
46
+ enabled.checked = false; prompts.checked = false;
47
+ try { localStorage.removeItem(key); } catch (_) { /* write reports failure */ }
48
+ write();
49
+ });
50
+ element.querySelector(".bs-reset").addEventListener("click", () => {
51
+ ready = false; clearTimeout(timer);
52
+ enabled.disabled = true; prompts.disabled = true;
53
+ element.querySelector(".bs-reset").disabled = true;
54
+ send("reset", null);
55
+ });
56
+ window.addEventListener("pagehide", write);
57
+ enabled.disabled = true; prompts.disabled = true;
58
+ element.querySelector(".bs-reset").disabled = true;
59
+ let record = null;
60
+ try {
61
+ const raw = localStorage.getItem(key);
62
+ if (raw && raw.length <= 100000) record = JSON.parse(raw);
63
+ if (record?.version === 1 && record.family === props.family) {
64
+ enabled.checked = record.enabled !== false;
65
+ prompts.checked = enabled.checked && record.prompts === true;
66
+ } else { record = null; }
67
+ } catch (_) { note("保存設定を読み込めないため、初期設定で開始します。"); }
68
+ // Strip opted-out prompts before even submitting a restoration request.
69
+ if (record && !prompts.checked && record.values && typeof record.values === "object") {
70
+ delete record.values.prompt; delete record.values.negative_prompt;
71
+ }
72
+ if (!enabled.checked) record = null;
73
+ // Favorites are independent of automatic settings storage and prompt consent.
74
+ const favoriteKey = `image-model-collection:favorites:v1:${props.family}`;
75
+ const favoriteList = element.querySelector(".bf-list");
76
+ const favoriteName = element.querySelector(".bf-name");
77
+ const favoriteSearch = element.querySelector(".bf-search");
78
+ let favorites = [], favoriteBusy = false, pendingFavorite = null;
79
+ const maxFavorites = 100;
80
+ const maxBytes = 5 * 1024 * 1024;
81
+ function favoriteNote(text) { element.querySelector(".bf-message").textContent = text; }
82
+ function validateFavorites(data) {
83
+ if (!data || !["anima-collection-plus-mcp-favorites", "imggen-favorites"].includes(data.format) || data.version !== 1 || data.family !== props.family
84
+ || !Array.isArray(data.entries) || data.entries.length > maxFavorites) throw Error("形式・モード・件数が一致しません。現在のモードのJSONを選択してください。");
85
+ return data.entries.map(entry => {
86
+ if (!entry || typeof entry.name !== "string" || !entry.name.trim() || entry.name.length > 80
87
+ || !entry.values || typeof entry.values !== "object" || Array.isArray(entry.values)) throw Error("お気に入りの名前・設定が不正です。");
88
+ const clean = {};
89
+ for (const [field, rule] of Object.entries(props.rules)) {
90
+ if (!Object.hasOwn(entry.values, field)) continue;
91
+ const value = entry.values[field];
92
+ if (field === "seed") {
93
+ const seed = String(value);
94
+ if (!/^-?\d{1,20}$/.test(seed) || BigInt(seed) < -1n || BigInt(seed) > 18446744073709551615n) throw Error("Seedが不正です。");
95
+ clean[field] = seed;
96
+ } else if (rule[0] === "choice") {
97
+ // Keep unavailable names as inert text; the server warns and falls back on Apply.
98
+ if (typeof value !== "string" || value.length > 300) throw Error("選択項目が不正です。");
99
+ clean[field] = value;
100
+ } else if (rule[0] === "bool") {
101
+ if (typeof value !== "boolean") throw Error("チェック項目が不正です。");
102
+ clean[field] = value;
103
+ } else if (rule[0] === "text") {
104
+ if (typeof value !== "string" || value.length > rule[1]) throw Error("プロンプト等の文字数が上限を超えています。");
105
+ clean[field] = value;
106
+ } else {
107
+ if (typeof value !== "number" || !Number.isFinite(value) || value < rule[1] || value > rule[2]) throw Error("数値設定が範囲外です。");
108
+ clean[field] = value;
109
+ }
110
+ }
111
+ return {id:crypto.randomUUID(), name:entry.name.trim(), values:clean};
112
+ });
113
+ }
114
+ function envelope(entries) { return {format:"anima-collection-plus-mcp-favorites", version:1, family:props.family, entries}; }
115
+ function loadFavorites() {
116
+ const raw = localStorage.getItem(favoriteKey);
117
+ if (!raw) return [];
118
+ if (new Blob([raw]).size > maxBytes) throw Error("保存データが大きすぎます。");
119
+ return validateFavorites(JSON.parse(raw));
120
+ }
121
+ function persistFavorites(next, selectedId) {
122
+ if (next.length > maxFavorites) throw Error(`最大${maxFavorites}件です。不要な項目を削除してください。`);
123
+ const raw = JSON.stringify(envelope(next));
124
+ if (new Blob([raw]).size > maxBytes) throw Error("保存容量の上限(5MB)を超えています。");
125
+ localStorage.setItem(favoriteKey, raw); // Change the UI only after persistence succeeds.
126
+ favorites = next; renderFavorites(selectedId);
127
+ }
128
+ function selectedFavorite() { return favorites.find(entry => entry.id === favoriteList.value); }
129
+ function renderFavorites(selectedId = favoriteList.value) {
130
+ const query = favoriteSearch.value.trim().normalize("NFKC").toLocaleLowerCase();
131
+ const visible = favorites.filter(entry => entry.name.normalize("NFKC").toLocaleLowerCase().includes(query));
132
+ favoriteList.replaceChildren();
133
+ const empty = document.createElement("option"); empty.value = ""; empty.textContent = visible.length ? `選択してください(${visible.length}件表示/全${favorites.length}件)` : query ? "一致するお気に入りがありません" : "選択してください(0件)"; favoriteList.append(empty);
134
+ visible.forEach(entry => { const option = document.createElement("option"); option.value = entry.id; option.textContent = entry.name; favoriteList.append(option); });
135
+ favoriteList.value = visible.some(entry => entry.id === selectedId) ? selectedId : "";
136
+ element.querySelector(".bf-count").textContent = `${visible.length}件表示/全${favorites.length}件(上限${maxFavorites}件)。エクスポート・全件クリアは検索結果ではなく全件が対象です。`;
137
+ for (const button of element.querySelectorAll(".bf-panel button")) button.disabled = !ready || favoriteBusy;
138
+ for (const selector of [".bf-apply", ".bf-update", ".bf-delete"]) element.querySelector(selector).disabled ||= !selectedFavorite();
139
+ element.querySelector(".bf-add").disabled ||= favorites.length >= maxFavorites;
140
+ }
141
+ function safely(fn) { try { fn(); } catch (error) { favoriteNote(`保存・読込に失敗しました:${error.message}`); } }
142
+ function captureFavorite(update) {
143
+ if (!ready || favoriteBusy) return;
144
+ const name = favoriteName.value.trim();
145
+ if (!name || name.length > 80) { favoriteNote("名前を1~80文字で入力してください。"); return; }
146
+ const selected = selectedFavorite();
147
+ if (update && (!selected || !confirm(`「${selected.name}」を現在の設定で上書きしますか?`))) return;
148
+ if (!update && favorites.length >= maxFavorites) { favoriteNote(`最大${maxFavorites}件です。不要な項目を削除してください。`); return; }
149
+ pendingFavorite = {request:crypto.randomUUID(), id:update ? selected.id : crypto.randomUUID(), name, update};
150
+ const requestId = pendingFavorite.request;
151
+ favoriteBusy = true; renderFavorites();
152
+ props.value = {action:"favorite_save", seed_source:element.querySelector(".bf-seed").value, request:pendingFavorite.request}; trigger("submit");
153
+ setTimeout(() => { if (favoriteBusy && pendingFavorite?.request === requestId) { favoriteBusy=false; pendingFavorite=null; renderFavorites(); favoriteNote("応���を確認できませんでした。保存済みの一覧を確認して再試行してください。"); } },15000);
154
+ }
155
+ function completeFavorite(data) {
156
+ if (!pendingFavorite || pendingFavorite.request !== data.request) return;
157
+ const request = pendingFavorite; pendingFavorite=null; favoriteBusy=false;
158
+ if (data.error) { renderFavorites(); favoriteNote(data.error); return; }
159
+ safely(() => {
160
+ const entry = {id:request.id, name:request.name, values:data.values};
161
+ const next = request.update ? favorites.map(item => item.id === entry.id ? entry : item) : [...favorites, entry];
162
+ persistFavorites(next, entry.id); favoriteNote("お気に入りを保存しました(プロンプトを含む・このブラウザのみ)。");
163
+ });
164
+ renderFavorites(request.id);
165
+ }
166
+ favoriteSearch.addEventListener("input", () => {
167
+ const previous = favoriteList.value; renderFavorites();
168
+ if (previous && !favoriteList.value) favoriteName.value = "";
169
+ });
170
+ favoriteList.addEventListener("change", () => { favoriteName.value = selectedFavorite()?.name || ""; renderFavorites(); });
171
+ element.querySelector(".bf-add").addEventListener("click", () => captureFavorite(false));
172
+ element.querySelector(".bf-update").addEventListener("click", () => captureFavorite(true));
173
+ element.querySelector(".bf-apply").addEventListener("click", () => {
174
+ const entry = selectedFavorite(); if (!entry || !ready || favoriteBusy) return;
175
+ favoriteNote("設定を読み込み中…");
176
+ send("favorite_apply", {version:1, family:props.family, enabled:true, prompts:true, values:entry.values});
177
+ });
178
+ element.querySelector(".bf-delete").addEventListener("click", () => safely(() => {
179
+ const entry=selectedFavorite(); if (!entry || !confirm(`「${entry.name}」を削除しますか?`)) return;
180
+ persistFavorites(favorites.filter(item => item.id !== entry.id)); favoriteName.value=""; favoriteNote("選択したお気に入りを削除しました。");
181
+ }));
182
+ element.querySelector(".bf-clear").addEventListener("click", () => safely(() => {
183
+ if (!confirm(`このモードのお気に入り${favorites.length}件をすべて削除しますか?エクスポート済みのJSONがない場合は復元できません。`)) return;
184
+ localStorage.removeItem(favoriteKey); favorites=[]; renderFavorites(); favoriteName.value=""; favoriteNote("このモードのお気に入りを全件削除しました。現在の入力欄と前回設定の保存は変更していません。");
185
+ }));
186
+ element.querySelector(".bf-export").addEventListener("click", () => safely(() => {
187
+ const url=URL.createObjectURL(new Blob([JSON.stringify(envelope(favorites),null,2)],{type:"application/json"}));
188
+ const a=document.createElement("a"); a.href=url; a.download=`anima-collection-plus-mcp-favorites-${props.family}.json`; a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000);
189
+ favoriteNote("JSONを出力しました。プロンプトが含まれるため共有にご注意ください。");
190
+ }));
191
+ element.querySelector(".bf-import").addEventListener("click", () => element.querySelector(".bf-file").click());
192
+ element.querySelector(".bf-file").addEventListener("change", async event => {
193
+ const file=event.target.files[0]; event.target.value=""; if (!file) return;
194
+ try {
195
+ if (file.size > maxBytes) throw Error("JSONは5MB以下にしてください。");
196
+ const added=validateFavorites(JSON.parse(await file.text()));
197
+ if (favorites.length + added.length > maxFavorites) throw Error(`追加すると${maxFavorites}件を超えます。削除してから再試行してください。`);
198
+ persistFavorites([...favorites,...added]); favoriteNote(`${added.length}件追加しました。同名の項目も既存データを上書きせず別の項目として追加しています。`);
199
+ } catch(error) { favoriteNote(`インポートできません:${error.message}`); }
200
+ });
201
+ try { favorites=loadFavorites(); } catch(error) { favoriteNote(`保存データを読み込めません:${error.message}(既存データは削除していません)`); }
202
+ renderFavorites();
203
+ send("restore", record);
browser_settings.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validated, per-browser settings; never a source of generation authorization."""
2
+
3
+ import math
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import gradio as gr
8
+
9
+ from jujo_loras import NONE_LORA
10
+
11
+ ASSETS = Path(__file__).with_name("assets") / "settings"
12
+ PROMPTS = {"prompt", "negative_prompt"}
13
+
14
+
15
+ class SettingsSchema:
16
+ def __init__(self, controls):
17
+ self.defaults = {key: component.value for key, component in controls.items()}
18
+ self.rules = {}
19
+ for key, component in controls.items():
20
+ if isinstance(component, gr.Checkbox):
21
+ rule = ("bool", key.startswith("lora") or component.interactive is not False)
22
+ elif isinstance(component, (gr.Dropdown, gr.Radio)):
23
+ rule = ("choice", [item[1] for item in component.choices])
24
+ elif isinstance(component, gr.Slider):
25
+ rule = ("number", component.minimum, component.maximum, component.step)
26
+ elif isinstance(component, gr.Number):
27
+ rule = ("number", -1, 2**64 - 1, 1)
28
+ else:
29
+ rule = ("text", 20000)
30
+ self.rules[key] = rule
31
+
32
+ def sanitize(self, raw, include_prompts=False):
33
+ raw = raw if isinstance(raw, dict) else {}
34
+ clean = dict(self.defaults)
35
+ for key, rule in self.rules.items():
36
+ if key not in raw:
37
+ continue
38
+ value = raw.get(key)
39
+ if key == "seed" and isinstance(value, str) and re.fullmatch(r"-?\d{1,20}", value):
40
+ value = int(value)
41
+ if key in PROMPTS and not include_prompts:
42
+ continue
43
+ if rule[0] == "bool":
44
+ clean[key] = value if type(value) is bool and rule[1] else False
45
+ elif rule[0] == "choice":
46
+ if value in rule[1]:
47
+ clean[key] = value
48
+ elif key.endswith("_file"):
49
+ clean[key] = NONE_LORA
50
+ elif rule[0] == "text":
51
+ if isinstance(value, str) and len(value) <= rule[1]:
52
+ clean[key] = value
53
+ elif type(value) in (int, float):
54
+ # Check bounds first: isfinite(very_large_int) can overflow.
55
+ if rule[1] <= value <= rule[2] and math.isfinite(value):
56
+ step = rule[3]
57
+ clean[key] = round(round(value / step) * step, 8) if step != 1 else int(value)
58
+ for index in range(1, 5):
59
+ if clean.get(f"lora{index}_file") == NONE_LORA:
60
+ clean[f"lora{index}_enabled"] = False
61
+ return clean
62
+
63
+
64
+ class SettingsStorage(gr.HTML):
65
+ def api_info(self):
66
+ return {"type": "object", "additionalProperties": True}
67
+
68
+
69
+ def bind_browser_settings(family, controls, preset_status=None, preset_specs=None, generation_info=None):
70
+ schema = SettingsSchema(controls)
71
+ ready = gr.State(False)
72
+ exact_seed = gr.State(None)
73
+ bridge = SettingsStorage(
74
+ value=None, family=family, field_names=list(controls), rules=schema.rules, defaults=schema.defaults,
75
+ html_template=(ASSETS / "settings.html").read_text(encoding="utf-8"),
76
+ css_template=(ASSETS / "settings.css").read_text(encoding="utf-8"),
77
+ js_on_load=(ASSETS / "settings.js").read_text(encoding="utf-8"),
78
+ )
79
+ components = list(controls.values())
80
+
81
+ def restore(payload, *current):
82
+ payload = payload if isinstance(payload, dict) else {}
83
+ action = payload.get("action")
84
+ if action == "favorite_save":
85
+ values = schema.sanitize(dict(zip(controls, current)), True)
86
+ if current[len(controls)] is not None:
87
+ values["seed"] = int(current[len(controls)])
88
+ error = ""
89
+ if payload.get("seed_source") == "last":
90
+ match = re.search(r"\| seed (\d+)\b", str(current[-1])) if generation_info is not None else None
91
+ if match:
92
+ values["seed"] = int(match[1])
93
+ else:
94
+ error = "このページではまだ生成が完了していません。入力Seedで保存するか、生成後に再試行してください。"
95
+ values["seed"] = str(values["seed"])
96
+ return (*[gr.skip() for _ in components],
97
+ *([gr.skip()] if preset_status is not None else []), gr.skip(), gr.skip(),
98
+ {"action": "favorite_saved", "values": values, "error": error,
99
+ "request": payload.get("request")})
100
+ reset = payload.get("action") == "reset"
101
+ record = payload.get("record")
102
+ valid = (isinstance(record, dict) and record.get("version") == 1
103
+ and record.get("family") == family and record.get("enabled") is True)
104
+ values = schema.sanitize(record.get("values"), record.get("prompts") is True) if valid and not reset else dict(schema.defaults)
105
+ warnings = []
106
+ if action == "favorite_apply" and valid:
107
+ raw = record.get("values", {})
108
+ raw = raw if isinstance(raw, dict) else {}
109
+ warnings = [key for key, value in raw.items() if key in schema.rules and schema.rules[key][0] == "choice" and value not in schema.rules[key][1]]
110
+ seed = str(values["seed"]) if valid and not reset else None
111
+ updates = []
112
+ for key in controls:
113
+ options = {"value": values[key]}
114
+ if key.startswith("lora") and (key.endswith("_enabled") or key.endswith("_strength")):
115
+ slot = key.split("_")[0]
116
+ options["interactive"] = values[f"{slot}_file"] != NONE_LORA
117
+ updates.append(gr.update(**options))
118
+ if preset_status is not None:
119
+ status = preset_specs[values["preset"]]["status"]
120
+ if valid and not reset:
121
+ status += "\n\n_保存された手動調整を含む設定を復元しました。プリセット名より現在の各項目の値が優先されます。_"
122
+ updates.append(status)
123
+ stored_values = dict(values, seed=str(values["seed"])) if action == "favorite_apply" else values
124
+ return (*updates, True, seed, {"action": "ready", "values": stored_values,
125
+ "favorite_applied": action == "favorite_apply",
126
+ "warning": "未登録の項目を初期値/未選択へ変更しました:" + "、".join(warnings) if warnings else ""})
127
+
128
+ def snapshot(initialized, saved_seed, *values):
129
+ if not initialized:
130
+ return gr.skip()
131
+ # Prompt exclusion happens in the browser; the current UI values are
132
+ # already session data and are not written to any server-side file.
133
+ current = dict(zip(controls, values))
134
+ if saved_seed is not None:
135
+ current["seed"] = str(saved_seed)
136
+ return {"action": "sync", "values": current}
137
+
138
+ bridge.submit(
139
+ restore, inputs=[bridge, *components, exact_seed, *([generation_info] if generation_info is not None else [])],
140
+ outputs=[*components, *([preset_status] if preset_status is not None else []), ready, exact_seed, bridge],
141
+ queue=False, api_visibility="private", show_progress="hidden",
142
+ )
143
+ gr.on(
144
+ triggers=[component.change for component in [*components, exact_seed]], fn=snapshot,
145
+ inputs=[ready, exact_seed, *components], outputs=bridge, queue=False,
146
+ trigger_mode="always_last", api_visibility="private", show_progress="hidden",
147
+ )
148
+ controls["seed"].input(lambda: None, outputs=exact_seed, queue=False, api_visibility="private")
149
+ return exact_seed
generation_stats.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Best-effort JST daily counts. No disk/network I/O on the generation path."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import threading
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+ JST = timezone(timedelta(hours=9), "Asia/Tokyo")
12
+
13
+
14
+ class GenerationStats:
15
+ """Single-process writer; deliberately not a distributed accounting DB."""
16
+
17
+ def __init__(self, directory=None, interval=300):
18
+ self.path = Path(directory) / "daily-counts.json" if directory else None
19
+ self.interval = interval
20
+ self._counts = {}
21
+ self._revision = self._saved_revision = 0
22
+ self._loaded = False
23
+ self._lock = threading.Lock()
24
+ self._io_lock = threading.Lock()
25
+ self._stop = threading.Event()
26
+ self._thread = None
27
+
28
+ def record(self, family, model, count=1, completed_at=None):
29
+ if type(count) is not int or count <= 0:
30
+ return
31
+ day = (completed_at or datetime.now(JST)).astimezone(JST).date().isoformat()
32
+ with self._lock:
33
+ models = self._counts.setdefault(day, {}).setdefault(family, {})
34
+ models[model] = models.get(model, 0) + count
35
+ self._revision += 1
36
+
37
+ def snapshot(self):
38
+ with self._lock:
39
+ return self._snapshot_unlocked()
40
+
41
+ def _load(self):
42
+ if self._loaded or self.path is None:
43
+ return
44
+ try:
45
+ if self.path.stat().st_size > 10_000_000:
46
+ raise ValueError("statistics file too large")
47
+ raw = json.loads(self.path.read_text(encoding="utf-8"))
48
+ if raw.get("version") != 1 or not isinstance(raw.get("counts"), dict):
49
+ raise ValueError("invalid statistics schema")
50
+ restored = {}
51
+ for day, families in raw["counts"].items():
52
+ datetime.strptime(day, "%Y-%m-%d")
53
+ restored[day] = {}
54
+ for family, models in families.items():
55
+ if family not in ("anima", "illustrious") or not isinstance(models, dict):
56
+ raise ValueError("invalid model family")
57
+ restored[day][family] = {}
58
+ for model, count in models.items():
59
+ if type(count) is not int or count < 0 or len(model) > 256:
60
+ raise ValueError("invalid model count")
61
+ restored[day][family][model] = count
62
+ except FileNotFoundError:
63
+ restored = {}
64
+ with self._lock:
65
+ # Counts generated during startup loading must not be lost.
66
+ for day, families in restored.items():
67
+ for family, models in families.items():
68
+ target = self._counts.setdefault(day, {}).setdefault(family, {})
69
+ for model, count in models.items():
70
+ target[model] = target.get(model, 0) + count
71
+ self._loaded = True
72
+
73
+ def flush(self):
74
+ if self.path is None:
75
+ return
76
+ with self._io_lock:
77
+ self._load()
78
+ with self._lock:
79
+ revision = self._revision
80
+ if revision == self._saved_revision:
81
+ return
82
+ data = self._snapshot_unlocked()
83
+ # Never hold the counter lock while accessing mounted storage.
84
+ self.path.parent.mkdir(parents=True, exist_ok=True)
85
+ temporary = self.path.with_suffix(".json.tmp")
86
+ temporary.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
87
+ os.replace(temporary, self.path)
88
+ with self._lock:
89
+ self._saved_revision = revision
90
+
91
+ def _snapshot_unlocked(self):
92
+ return {"version": 1, "timezone": "Asia/Tokyo",
93
+ "counts": {day: {family: dict(models) for family, models in families.items()}
94
+ for day, families in self._counts.items()}}
95
+
96
+ def start(self):
97
+ if self._thread is not None:
98
+ return
99
+ if self.path is None:
100
+ LOGGER.warning("GENERATION_STATS_DIR is unset; counts are memory-only until a persistent directory is configured.")
101
+ return
102
+
103
+ def worker():
104
+ try:
105
+ with self._io_lock:
106
+ self._load()
107
+ except Exception:
108
+ LOGGER.warning("Statistics restore failed; will retry without blocking generation.", exc_info=True)
109
+ while not self._stop.wait(self.interval):
110
+ try:
111
+ self.flush()
112
+ except Exception:
113
+ LOGGER.warning("Statistics save failed; will retry at the next interval.", exc_info=True)
114
+
115
+ self._thread = threading.Thread(target=worker, name="generation-statistics", daemon=True)
116
+ self._thread.start()
117
+
118
+ def stop(self):
119
+ self._stop.set()
120
+ if self._thread is not None:
121
+ self._thread.join(timeout=1)
122
+
123
+
124
+ stats = GenerationStats(os.environ.get("GENERATION_STATS_DIR"))
jujo_browser.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Responsive, WebUI-only author sample browser and explicit slot assignment."""
2
+
3
+ import math
4
+ import re
5
+ from pathlib import Path
6
+ from urllib.parse import quote
7
+
8
+ import gradio as gr
9
+
10
+ from jujo_loras import CATALOGS, JUJO_ENTRIES, NONE_LORA, PONY_LABEL, PONY_WARNING, lora_selected
11
+
12
+
13
+ ASSETS = Path(__file__).resolve().parent / "assets" / "jujo-browser"
14
+
15
+
16
+ def category(entry):
17
+ series = Path(entry["doc"]).parent.name
18
+ if series in {"doyagao", "inv_v_eyebrows"}:
19
+ return "眉"
20
+ if series in {"smallmouth", "widemouth", "mouthcollection", "talkmouth", "smallclosedmouth",
21
+ "smirkmouth", "smugmouth", "chestnutmouth", "circlemouth", "clenchedteeth"}:
22
+ return "口"
23
+ if series in {"darkness", "facelightup", "anti25d", "make25d"}:
24
+ return "効果"
25
+ if series in {"eyeshapes", "eyecollexl", "tareme", "tsurime", "jitome", "hosome", "closedeye",
26
+ "coldeyes", "pupil", "smirkingeye", "worriedeyes", "wink", "sleepy", "surprised",
27
+ "whiteeyelash", "eyelashext"} or entry["name"].startswith("hotarueye_"):
28
+ return "目の形"
29
+ return "表情・人物"
30
+
31
+
32
+ def entries_for_browser(family):
33
+ catalog = CATALOGS[family]
34
+ base = f"https://huggingface.co/{catalog['repo']}"
35
+ entries = []
36
+ for entry in JUJO_ENTRIES[family].values():
37
+ name = entry["name"]
38
+ pony_only = bool(entry.get("pony_only"))
39
+ code = name.removesuffix(".safetensors")
40
+ for suffix in ("_anima_v10", "_xl_v10", "_xl_v20", "_xl_v30"):
41
+ code = code.removesuffix(suffix)
42
+ hint = "強度や補助プロンプトは、作者の使い方をご確認ください。"
43
+ sample_note = "作者のシリーズ共通比較サンプル" if entry["group_sample"] else "作者の生成サンプル"
44
+ strength = 0.8 if family == "anima" else 1.0
45
+ pupil = re.fullmatch(r"hotarupupil_xl_type([1-6])_v10\.safetensors", name)
46
+ hosome = re.fullmatch(r"hotarueye_xl_hosome([12])_v30\.safetensors", name)
47
+ comic = re.fullmatch(r"hotarueye_xl_comic(0[1-4])_v\d+(?:_(?:ill|pony))?\.safetensors", name)
48
+ if family == "anima" and entry["doc"] == "eyeshapes/README.md":
49
+ hint = "作者の強度目安:0.3~1.5。複数併用時は合計1.0~1.5程度。closed eyesは避けてください。"
50
+ elif family == "illustrious" and entry["doc"] == "sdxl/eyecollexl/README.md":
51
+ hint = "作者のILL向け強度目安:1.5~2.0。種類に応じてtareme / tsurimeも参照してください。"
52
+ strength = 1.5
53
+ eye_code = re.fullmatch(r"eyecolle_xl_(code\d{3})", code)
54
+ if eye_code:
55
+ code = eye_code[1]
56
+ if entry["image"] and not entry["group_sample"]:
57
+ shape = "tareme" if Path(entry["image"]).name.startswith("tareme_") else "tsurime"
58
+ shape_label = "タレ目系" if shape == "tareme" else "吊り目系"
59
+ sample_note = f"{code}に対応する作者のIllustrious生成サンプル({shape_label})"
60
+ hint = f"作者のILL向け強度目安:1.5~2.0。掲載サンプルは{shape}をプロンプトに指定しています。"
61
+ elif family == "illustrious" and pupil:
62
+ code = f"Type {pupil[1]}"
63
+ sample_note = f"作者の比較画像から{code}の部分を表示(原図の比較画像も開けます)"
64
+ descriptions = {
65
+ "1": "大きな瞳・白目が少ないタイプ", "2": "寄り目・左右方向が細い瞳",
66
+ "3": "寄り目・正円に近い瞳", "4": "ハイライトが少なく黒目がちな印象",
67
+ "5": "左右方向が細く、上下端のカーブが見える瞳",
68
+ "6": "左右方向が細く、上下端のカーブが隠れる瞳",
69
+ }
70
+ hint = f"{code}:{descriptions[pupil[1]]}。作者の強度目安:0.5~2.0。追加プロンプトは基本的に不要です。"
71
+ elif family == "illustrious" and hosome:
72
+ code = f"Type {hosome[1]} / v3.0"
73
+ shape = "吊り目型・直線的な目尻" if hosome[1] == "1" else "タレ目型・曲線的な目尻"
74
+ sample_note = f"Type {hosome[1]}に対応する作者のv3.0サンプル(Illustrious向け)"
75
+ hint = f"{shape}。作者の強度目安:0.5~1.5。追加プロンプトは基本的に不要です。"
76
+ elif family == "illustrious" and comic:
77
+ code = f"Type {int(comic[1])} / {'Pony' if pony_only else 'ILL'}"
78
+ sample_note = f"Type {int(comic[1])}に対応する作者のタイプ別サンプル"
79
+ hint = "目の色や形などの詳細な記述はプロンプトに入れずに使用してください。"
80
+ if pony_only:
81
+ sample_note += "(Pony用LoRAの作者掲載画像。ILLでの再現は未確認)"
82
+ entries.append(dict(
83
+ name=name, title=entry["title"], code=code, category=category(entry),
84
+ image=f"{base}/resolve/{catalog['revision']}/{quote(entry['image'])}" if entry["image"] else None,
85
+ source=f"{base}/blob/{catalog['revision']}/{quote(entry['doc'])}",
86
+ group_sample=entry["group_sample"], sample_note=sample_note, hint=hint, strength=strength,
87
+ sample_viewport=entry.get("sample_viewport"),
88
+ pony_only=pony_only, compatibility_label=PONY_LABEL if pony_only else "",
89
+ compatibility_warning=PONY_WARNING if pony_only else "",
90
+ ))
91
+ return entries
92
+
93
+
94
+ def slot_snapshot(values):
95
+ return [dict(enabled=lora_selected(values[i], values[i + 1]),
96
+ name=values[i + 1] or NONE_LORA, strength=float(values[i + 2]))
97
+ for i in range(0, 12, 3)]
98
+
99
+
100
+ def assignment(family, payload, values):
101
+ """Validate the explicit Apply action using live Gradio slot values."""
102
+ if not isinstance(payload, dict) or payload.get("selected") not in JUJO_ENTRIES[family]:
103
+ raise gr.Error("このモードのサンプルからLoRAを選択してください。")
104
+ slot = payload.get("slot")
105
+ if isinstance(slot, bool) or not isinstance(slot, int) or not 1 <= slot <= 4:
106
+ raise gr.Error("適用先のLoRA 1~4を選択してください。")
107
+ try:
108
+ strength = float(payload["strength"])
109
+ except (KeyError, TypeError, ValueError):
110
+ raise gr.Error("強度を0~2の範囲で入力してください。") from None
111
+ if not math.isfinite(strength) or not 0 <= strength <= 2:
112
+ raise gr.Error("強度を0~2の範囲で入力してください。")
113
+ updated = list(values)
114
+ offset = (slot - 1) * 3
115
+ updated[offset:offset + 3] = [True, payload["selected"], strength]
116
+ return offset, updated
117
+
118
+
119
+ class JujoBrowser(gr.HTML):
120
+ def api_info(self):
121
+ return {"type": "object", "additionalProperties": True}
122
+
123
+
124
+ def build_jujo_browser(family, lora_components, lora_defaults):
125
+ initial = [value for slot in lora_defaults for value in slot]
126
+ with gr.Accordion("JujoHotaruのLoRAサンプル・使い方はこちら", open=False, elem_classes=["jujo-samples"]):
127
+ browser = JujoBrowser(
128
+ value={"slots": slot_snapshot(initial)}, entries=entries_for_browser(family),
129
+ html_template=(ASSETS / "browser.html").read_text(encoding="utf-8"),
130
+ css_template=(ASSETS / "browser.css").read_text(encoding="utf-8"),
131
+ js_on_load=(ASSETS / "browser.js").read_text(encoding="utf-8"),
132
+ apply_default_css=False, padding=False,
133
+ )
134
+
135
+ def sync_slots(*values):
136
+ return {"slots": slot_snapshot(values)}
137
+
138
+ def apply_sample(payload, *values):
139
+ offset, updated = assignment(family, payload, values)
140
+ outputs = [gr.skip() for _ in values]
141
+ outputs[offset:offset + 3] = [
142
+ gr.Checkbox(value=True, interactive=True),
143
+ gr.Dropdown(value=updated[offset + 1]),
144
+ gr.Slider(value=updated[offset + 2], interactive=True),
145
+ ]
146
+ return (*outputs, {"slots": slot_snapshot(updated),
147
+ "message": f"LoRA {offset // 3 + 1}に設定して有効化しました。"})
148
+
149
+ gr.on(
150
+ triggers=[component.change for component in lora_components], fn=sync_slots,
151
+ inputs=lora_components, outputs=browser, queue=False,
152
+ trigger_mode="always_last", api_visibility="private",
153
+ )
154
+ browser.submit(
155
+ apply_sample, inputs=[browser, *lora_components],
156
+ outputs=[*lora_components, browser], queue=False,
157
+ api_visibility="private",
158
+ )
159
+ return browser
jujo_loras.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pinned JujoHotaru catalog, optional slot selection and sample cards."""
2
+
3
+ import html
4
+ import json
5
+ from pathlib import Path
6
+ from urllib.parse import quote
7
+
8
+
9
+ NONE_LORA = "---"
10
+ PONY_LABEL = "Pony用・ILL未検証"
11
+ PONY_WARNING = (
12
+ "Pony系モデル向けのSDXL LoRAです。Illustriousで読み込めても、効き方・画質・再現性は未検証です。"
13
+ "効果が弱い、意図しない描写になる場合があります。掲載画像もILLでの生成結果とは限りません。"
14
+ )
15
+ CATALOG_ROOT = Path(__file__).resolve().parent / "assets" / "jujo"
16
+ CATALOGS = {
17
+ family: json.loads((CATALOG_ROOT / f"{family}.json").read_text(encoding="utf-8"))
18
+ for family in ("anima", "illustrious")
19
+ }
20
+ JUJO_ENTRIES = {
21
+ family: {entry["name"]: entry for entry in sorted(catalog["entries"], key=lambda entry: bool(entry.get("pony_only")))}
22
+ for family, catalog in CATALOGS.items()
23
+ }
24
+
25
+
26
+ def lora_selected(enabled, name):
27
+ """The no-selection value is inactive even when Enable is checked."""
28
+ return bool(enabled and name and name != NONE_LORA)
29
+
30
+
31
+ def jujo_label(family, name):
32
+ entry = JUJO_ENTRIES[family].get(name)
33
+ prefix = f"[{PONY_LABEL}] " if entry and entry.get("pony_only") else ""
34
+ return f"{prefix}JujoHotaru / {entry['title']} / {name}" if entry else name
35
+
36
+
37
+ def pony_selection_notice(family, names):
38
+ selected = [name for name in names if JUJO_ENTRIES.get(family, {}).get(name, {}).get("pony_only")]
39
+ if not selected:
40
+ return ""
41
+ return f"> **注意:{PONY_LABEL}** \n> {PONY_WARNING} \n> 選択中:" + "、".join(f"`{name}`" for name in dict.fromkeys(selected))
42
+
43
+
44
+ def sample_card(family, name):
45
+ entry = JUJO_ENTRIES[family].get(name)
46
+ if not entry:
47
+ return "<p>LoRAを選択すると作者のサンプルと配布元を表示します。</p>"
48
+ catalog = CATALOGS[family]
49
+ base = f"https://huggingface.co/{catalog['repo']}"
50
+ doc_url = f"{base}/blob/{catalog['revision']}/{quote(entry['doc'])}"
51
+ image_url = f"{base}/resolve/{catalog['revision']}/{quote(entry['image'])}" if entry["image"] else None
52
+ note = "シリーズ共通の比較サンプル" if entry["group_sample"] else "選択したLoRAのサンプル"
53
+ image = (
54
+ f'<a href="{image_url}" target="_blank" rel="noopener noreferrer">'
55
+ f'<img src="{image_url}" loading="lazy" referrerpolicy="no-referrer" '
56
+ f'alt="{html.escape(entry["title"], quote=True)}" '
57
+ 'style="max-width:100%;max-height:520px;object-fit:contain;border-radius:8px"></a>'
58
+ if image_url else "<p>サンプル画像は配布元で確認してください。</p>"
59
+ )
60
+ hint = (
61
+ "眼の形シリーズは強度0.3~1.5が作者の目安です。複数併用時は合計1.0~1.5を目安にしてください。"
62
+ if family == "anima" and entry["doc"] == "eyeshapes/README.md"
63
+ else "このアイコレクションXLはILLでは強度1.5~2.0が作者の目安です。タイプに応じてtareme / tsurimeも参照してください。"
64
+ if family == "illustrious" and entry["doc"] == "sdxl/eyecollexl/README.md"
65
+ else "強度・補助プロンプトの推奨は下の作者説明を確認してください。"
66
+ )
67
+ return (
68
+ f'<h4>{html.escape(entry["title"])}</h4><p><code>{html.escape(name)}</code></p>'
69
+ + (f'<p><strong>{PONY_LABEL}</strong>:{PONY_WARNING}</p>' if entry.get("pony_only") else "")
70
+ + f'<p>{note} · JujoHotaru(十条蛍)作成。掲載画像は作者の生成例です。</p>'
71
+ f'{image}<p>{hint}</p>'
72
+ f'<p><a href="{doc_url}" target="_blank" rel="noopener noreferrer">作者の使い方・サンプル・配布元を開く</a></p>'
73
+ )
74
+
75
+
76
+ def jujo_credits(family):
77
+ catalog = CATALOGS[family]
78
+ license_text = (
79
+ "CircleStone Labs Non-Commercial License(非商用)。"
80
+ "[ライセンス](https://huggingface.co/JujoHotaru/anima_lora/blob/main/LICENSE.md) / "
81
+ "[NOTICE](https://huggingface.co/JujoHotaru/anima_lora/blob/main/NOTICE.md)"
82
+ if family == "anima" else
83
+ "作者配布条件はMITまたはCreativeML Open RAIL-Mのデュアルライセンス。"
84
+ "[ライセンス](https://huggingface.co/JujoHotaru/lora/blob/main/LICENSE)"
85
+ )
86
+ return (
87
+ "\n\n### JujoHotaru LoRA・掲載サンプル\n\n"
88
+ f"- 作者: **JujoHotaru(十条蛍 / Hotaru Jujo)**\n"
89
+ f"- 配布元: [{catalog['repo']}]({catalog['source']})\n"
90
+ f"- 登録: {len(catalog['entries'])}種類。掲載サンプル画像も作者のリポジトリを参照\n"
91
+ + (f"- Pony用を含みます。`{PONY_LABEL}` 表示のものはILLでの互換性を保証しません\n" if family == "illustrious" else "")
92
+ + f"- {license_text}\n"
93
+ "- 個別ファイルの説明・推奨強度はLoRAサンプル欄の配布元リンクを確認してください。\n"
94
+ )
lora_ui.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .lora-accordion {
2
+ border: 2px solid #7286c8 !important;
3
+ border-radius: 10px !important;
4
+ box-shadow: 0 2px 7px #4b69ad20;
5
+ }
6
+ .lora-accordion > button {
7
+ background: var(--background-fill-secondary, #eef2ff) !important;
8
+ padding: 14px 16px !important;
9
+ font-size: 1.05rem !important;
10
+ font-weight: 700 !important;
11
+ }
12
+ .jujo-samples { border: 1px solid #94a3b8 !important; }
requirements.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Anima Collection Gradio app (ComfyUI backend) for Hugging Face Spaces
2
+ # gradio / spaces / huggingface_hub / torch are preinstalled on the ZeroGPU
3
+ # base image: do NOT list them here (platform-pinned).
4
+ # torchvision / torchaudio: leave unpinned so pip matches the installed torch.
5
+
6
+ torchvision
7
+ torchaudio
8
+
9
+ # ComfyUI backend runtime deps (current master requires these at import time)
10
+ safetensors>=0.4.2
11
+ torchsde
12
+ einops
13
+ transformers>=4.50.3
14
+ tokenizers>=0.13.3
15
+ sentencepiece
16
+ aiohttp>=3.11.8
17
+ yarl>=1.18.0
18
+ pyyaml
19
+ Pillow
20
+ scipy
21
+ tqdm
22
+ psutil
23
+ filelock
24
+ requests
25
+ simpleeval>=1.0.0
26
+ blake3
27
+ comfy-aimdo==0.4.13
28
+ comfy-kitchen==0.2.30
29
+ alembic
30
+ SQLAlchemy>=2.0.0
31
+ av>=16.0.0
32
+ gradio[mcp,oauth]==6.23.1
scripts/update_jujo_catalog.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Snapshot author-published JujoHotaru LoRAs and sample references.
2
+
3
+ Run from the repo root. Reads Hub metadata/Markdown, never model weights.
4
+ SDXL Pony-targeted releases are included with explicit compatibility metadata.
5
+ SD1.5 and superseded versions are excluded from the Illustrious page.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import re
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from pathlib import Path, PurePosixPath
13
+ from urllib.parse import unquote, urljoin, urlparse
14
+
15
+ from huggingface_hub import HfApi
16
+
17
+
18
+ OUTPUT = Path(__file__).resolve().parents[1] / "assets" / "jujo"
19
+
20
+
21
+ def read(repo, revision, path):
22
+ import requests
23
+
24
+ response = requests.get(
25
+ f"https://huggingface.co/{repo}/resolve/{revision}/{path}", timeout=45
26
+ )
27
+ response.raise_for_status()
28
+ return response.text
29
+
30
+
31
+ def without_old_versions(text):
32
+ # Remove only details blocks explicitly labelled as old releases.
33
+ return re.sub(
34
+ r"<details[^>]*>\s*<summary[^>]*>[^<]*(?:旧|[Oo]ld versions)[^<]*</summary>.*?</details>",
35
+ "", text, flags=re.S,
36
+ )
37
+
38
+
39
+ def linked_weights(text, repo, doc):
40
+ found = []
41
+ for match in re.finditer(r"\]\(([^\n)]+\.safetensors(?:\?[^)]*)?)\)", text):
42
+ link = match[1]
43
+ url = urljoin(f"https://huggingface.co/{repo}/resolve/main/{doc}", link)
44
+ if f"/{repo}/resolve/" not in url:
45
+ continue
46
+ path = unquote(urlparse(url).path.split("/resolve/", 1)[1].split("/", 1)[1])
47
+ found.append(path)
48
+ return list(dict.fromkeys(found))
49
+
50
+
51
+ def is_pony_release(path, section):
52
+ return "_pony" in PurePosixPath(path).name or bool(re.search(
53
+ r"このバージョンはPony|[Pp]ony.only", section,
54
+ ))
55
+
56
+
57
+ def image_path(text, repo, revision, doc, path, available):
58
+ base = f"https://huggingface.co/{repo}/resolve/{revision}/{doc}"
59
+ images = []
60
+ for match in re.finditer(r"!\[[^\]]*\]\(([^\n]+?)\)", text):
61
+ url = urljoin(base, match[1])
62
+ prefix = f"https://huggingface.co/{repo}/resolve/{revision}/"
63
+ if url.startswith(prefix):
64
+ candidate = unquote(url[len(prefix):])
65
+ if candidate in available:
66
+ images.append(candidate)
67
+ stem = PurePosixPath(path).stem
68
+ short_stem = re.sub(r"_(?:ill|pony)$", "", stem)
69
+ # The author uses type/version names rather than weight basenames here.
70
+ hosome = re.fullmatch(r"hotarueye_xl_hosome([12])_v(\d+)", stem)
71
+ pupil = re.fullmatch(r"hotarupupil_xl_type([1-6])_v10", stem)
72
+ if doc == "sdxl/hosome/README.md" and hosome:
73
+ expected = f"type{hosome[1]}_v{hosome[2]}"
74
+ for candidate in images:
75
+ if PurePosixPath(candidate).stem == expected:
76
+ return candidate, False
77
+ if doc == "sdxl/pupil/README.md" and pupil:
78
+ for candidate in images:
79
+ if PurePosixPath(candidate).name == "v10_sample1.webp":
80
+ return candidate, True # comparison sheet; the UI frames one Type
81
+ eye_code = re.fullmatch(r"eyecolle_xl_code(\d{3})", stem)
82
+ if doc == "sdxl/eyecollexl/README.md" and eye_code:
83
+ # Author filenames use the code, not the LoRA's complete basename.
84
+ # Choose the ILL example; adjacent _pny images are Pony examples.
85
+ for candidate in images:
86
+ if re.fullmatch(r"(?:tareme|tsurime)_ill" + eye_code[1], PurePosixPath(candidate).stem):
87
+ return candidate, False
88
+ for candidate in images:
89
+ if PurePosixPath(candidate).stem == stem:
90
+ return candidate, False
91
+ for candidate in images:
92
+ if PurePosixPath(candidate).stem == short_stem:
93
+ return candidate, False
94
+ stems = [stem, re.sub(r"_(?:ill|pony)$", "", stem)]
95
+ for candidate in images:
96
+ if any(PurePosixPath(candidate).stem.startswith(s + "_") for s in stems):
97
+ return candidate, False
98
+ thumb = str(PurePosixPath(doc).parent / "thumb.webp")
99
+ if thumb in available:
100
+ return thumb, True
101
+ return (images[0], True) if images else (None, True)
102
+
103
+
104
+ def pupil_sample_viewport(path, image):
105
+ """Author's v10_sample1 layout: Original at left, Type1-6 in a 3x2 grid.
106
+
107
+ Presentation coordinates only: no author image is downloaded or rewritten.
108
+ This pinned source is 1968x800, with six 400x400 Type cells after x=768.
109
+ """
110
+ match = re.fullmatch(r"hotarupupil_xl_type([1-6])_v10", PurePosixPath(path).stem)
111
+ if not match or image != "sdxl/pupil/v10_sample1.webp":
112
+ return None
113
+ index = int(match[1]) - 1
114
+ return dict(x=768 + (index % 3) * 400, y=(index // 3) * 400,
115
+ width=400, height=400, source_width=1968, source_height=800)
116
+
117
+
118
+ def snapshot(repo, family):
119
+ api = HfApi()
120
+ revision = api.repo_info(repo).sha
121
+ tree = list(api.list_repo_tree(repo, revision=revision, recursive=True))
122
+ available = {item.path for item in tree}
123
+ sizes = {item.path: item.size for item in tree if hasattr(item, "size")}
124
+ prefix = "sdxl/" if family == "illustrious" else ""
125
+ docs = sorted(p for p in available if p.startswith(prefix) and p.endswith("README.md"))
126
+ with ThreadPoolExecutor(max_workers=6) as pool:
127
+ contents = dict(zip(docs, pool.map(lambda p: read(repo, revision, p), docs)))
128
+ entries = {}
129
+ for doc, text in contents.items():
130
+ cleaned = without_old_versions(text)
131
+ sections = re.split(r"(?=^#{1,4} )", cleaned, flags=re.M)
132
+ for section in sections:
133
+ paths = linked_weights(section, repo, doc)
134
+ for path in paths:
135
+ if path not in available or not path.startswith(prefix):
136
+ continue
137
+ pony_only = family == "illustrious" and is_pony_release(path, section)
138
+ title = text.splitlines()[0].lstrip("# ").split(" / ")[0]
139
+ if title in ("---", "SDXL LoRA series"):
140
+ title = section.splitlines()[0].lstrip("# ").split(" / ")[0]
141
+ image, group_sample = image_path(text, repo, revision, doc, path, available)
142
+ # A specific per-series document takes precedence over the index.
143
+ if path in entries and len(entries[path]["doc"]) > len(doc):
144
+ continue
145
+ entries[path] = dict(
146
+ name=PurePosixPath(path).name, path=path, title=title,
147
+ doc=doc, image=image, group_sample=group_sample,
148
+ size=sizes[path],
149
+ )
150
+ if pony_only:
151
+ entries[path]["pony_only"] = True
152
+ viewport = pupil_sample_viewport(path, image)
153
+ if viewport:
154
+ entries[path]["sample_viewport"] = viewport
155
+ # Every published Anima weight is compatible; include unlinked releases too.
156
+ if family == "anima":
157
+ for path in available:
158
+ if path.endswith(".safetensors") and path not in entries:
159
+ entries[path] = dict(name=PurePosixPath(path).name, path=path,
160
+ title="Anima LoRA", doc="README.md", image=None,
161
+ group_sample=True, size=sizes[path])
162
+ else:
163
+ # Keep only the newest author-published version for a given type.
164
+ newest = {}
165
+ for path, entry in entries.items():
166
+ key = (re.sub(r"_v\d+", "", path).replace("_ill", "").replace("_pony", ""),
167
+ bool(entry.get("pony_only")))
168
+ version = re.search(r"_v(\d+)", path)
169
+ rank = (int(version[1]) if version else 0, "_ill" in path)
170
+ if key not in newest or rank > newest[key][0]:
171
+ newest[key] = rank, entry
172
+ entries = {entry["path"]: entry for _, entry in newest.values()}
173
+ return dict(repo=repo, revision=revision,
174
+ source=f"https://huggingface.co/{repo}" + ("/blob/main/sdxl/README.md" if prefix else ""),
175
+ entries=sorted(entries.values(), key=lambda item: (bool(item.get("pony_only")), item["title"], item["name"])),
176
+ pony_paths=sorted(entry["path"] for entry in entries.values() if entry.get("pony_only")))
177
+
178
+
179
+ def refresh_eyecolle_samples():
180
+ """Update only sample references, keeping weights and revision pinned."""
181
+ destination = OUTPUT / "illustrious.json"
182
+ catalog = json.loads(destination.read_text(encoding="utf-8"))
183
+ doc = "sdxl/eyecollexl/README.md"
184
+ text = read(catalog["repo"], catalog["revision"], doc)
185
+ available = {item.path for item in HfApi().list_repo_tree(
186
+ catalog["repo"], revision=catalog["revision"], path_in_repo=str(PurePosixPath(doc).parent),
187
+ )}
188
+ updated = 0
189
+ for entry in catalog["entries"]:
190
+ if entry["doc"] != doc:
191
+ continue
192
+ image, group = image_path(text, catalog["repo"], catalog["revision"], doc, entry["path"], available)
193
+ if not image or group:
194
+ raise ValueError(f"Missing per-code Illustrious sample: {entry['name']}")
195
+ entry.update(image=image, group_sample=False)
196
+ updated += 1
197
+ destination.write_text(json.dumps(catalog, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
198
+ print(f"Updated {updated} Eye Collection XL ILL sample references; pinned revision unchanged.")
199
+
200
+
201
+ def refresh_eye_type_samples():
202
+ """Refresh only registered comic/pupil/half-closed types, not weights."""
203
+ destination = OUTPUT / "illustrious.json"
204
+ catalog = json.loads(destination.read_text(encoding="utf-8"))
205
+ targets = [entry for entry in catalog["entries"] if re.fullmatch(
206
+ r"(?:hotarueye_xl_comic(?:01|04)_v\d+_ill|hotarupupil_xl_type[1-6]_v10|hotarueye_xl_hosome[12]_v30)\.safetensors",
207
+ entry["name"],
208
+ )]
209
+ docs = sorted({entry["doc"] for entry in targets})
210
+ texts = {doc: read(catalog["repo"], catalog["revision"], doc) for doc in docs}
211
+ api = HfApi()
212
+ available = {item.path for doc in docs for item in api.list_repo_tree(
213
+ catalog["repo"], revision=catalog["revision"], path_in_repo=str(PurePosixPath(doc).parent),
214
+ )}
215
+ for entry in targets:
216
+ image, group = image_path(texts[entry["doc"]], catalog["repo"], catalog["revision"],
217
+ entry["doc"], entry["path"], available)
218
+ viewport = pupil_sample_viewport(entry["path"], image)
219
+ if not image or (group and not viewport):
220
+ raise ValueError(f"Missing type-specific sample: {entry['name']}")
221
+ entry.update(image=image, group_sample=group)
222
+ if viewport:
223
+ entry["sample_viewport"] = viewport
224
+ destination.write_text(json.dumps(catalog, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
225
+ print(f"Updated {len(targets)} comic/pupil/half-closed sample references; pinned revision unchanged.")
226
+
227
+
228
+ def register_pony_releases():
229
+ """Add formerly excluded SDXL Pony releases, preserving all existing entries."""
230
+ destination = OUTPUT / "illustrious.json"
231
+ catalog = json.loads(destination.read_text(encoding="utf-8"))
232
+ expected = set(catalog.get("pony_paths", catalog.get("excluded_pony", [])))
233
+ if not expected or any(not path.startswith("sdxl/") for path in expected):
234
+ raise ValueError("Expected a pinned list of SDXL Pony releases")
235
+ tree = list(HfApi().list_repo_tree(catalog["repo"], revision=catalog["revision"], path_in_repo="sdxl", recursive=True))
236
+ available = {item.path for item in tree}
237
+ sizes = {item.path: item.size for item in tree if hasattr(item, "size")}
238
+ added = {}
239
+ sections = []
240
+ for doc in ("sdxl/README.md", "sdxl/human_ear_eraser/README.md"):
241
+ text = read(catalog["repo"], catalog["revision"], doc)
242
+ sections.extend((doc, section) for section in re.split(r"(?=^#{1,4} )", without_old_versions(text), flags=re.M))
243
+ for doc, section in sections:
244
+ for path in linked_weights(section, catalog["repo"], doc):
245
+ if path not in expected or path not in available or not is_pony_release(path, section):
246
+ continue
247
+ # Restrict sample search to this type's section, not the shared header.
248
+ image, group = image_path(section, catalog["repo"], catalog["revision"], doc, path, available)
249
+ if not image:
250
+ raise ValueError(f"Missing author sample for {path}")
251
+ added[path] = dict(name=PurePosixPath(path).name, path=path,
252
+ title=section.splitlines()[0].lstrip("# ").split(" / ")[0],
253
+ doc=doc, image=image, group_sample=group, size=sizes[path], pony_only=True)
254
+ if set(added) != expected:
255
+ raise ValueError(f"Could not resolve pinned Pony releases: {sorted(expected - set(added))}")
256
+ existing = [entry for entry in catalog["entries"] if entry["path"] not in expected]
257
+ catalog["entries"] = existing + sorted(added.values(), key=lambda entry: (entry["title"], entry["name"]))
258
+ catalog.pop("excluded_pony", None)
259
+ catalog["pony_paths"] = sorted(expected)
260
+ destination.write_text(json.dumps(catalog, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
261
+ print(f"Registered {len(added)} SDXL Pony releases with warnings; existing entries and pinned revision unchanged.")
262
+
263
+
264
+ if __name__ == "__main__":
265
+ parser = argparse.ArgumentParser(description=__doc__)
266
+ parser.add_argument("--eyecolle-samples-only", action="store_true")
267
+ parser.add_argument("--eye-types-samples-only", action="store_true")
268
+ parser.add_argument("--register-pony", action="store_true")
269
+ arguments = parser.parse_args()
270
+ if arguments.eyecolle_samples_only:
271
+ refresh_eyecolle_samples()
272
+ raise SystemExit(0)
273
+ if arguments.eye_types_samples_only:
274
+ refresh_eye_type_samples()
275
+ raise SystemExit(0)
276
+ if arguments.register_pony:
277
+ register_pony_releases()
278
+ raise SystemExit(0)
279
+ OUTPUT.mkdir(parents=True, exist_ok=True)
280
+ for repo, family in (("JujoHotaru/anima_lora", "anima"), ("JujoHotaru/lora", "illustrious")):
281
+ catalog = snapshot(repo, family)
282
+ (OUTPUT / f"{family}.json").write_text(
283
+ json.dumps(catalog, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
284
+ )
285
+ licenses = ["LICENSE.md", "NOTICE.md"] if family == "anima" else ["LICENSE"]
286
+ for license_name in licenses:
287
+ (OUTPUT / f"{family}-{license_name}").write_text(
288
+ read(repo, catalog["revision"], license_name), encoding="utf-8"
289
+ )
290
+ print(f"{family}: {len(catalog['entries'])} entries, "
291
+ f"{sum(item['size'] for item in catalog['entries'])/1e9:.2f} GB, "
292
+ f"{sum(not item['image'] for item in catalog['entries'])} missing samples")
tag_completion.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """WebUI-only autocomplete around standard Gradio prompt textboxes."""
2
+
3
+ from pathlib import Path
4
+
5
+ import gradio as gr
6
+
7
+
8
+ ASSETS = Path(__file__).resolve().parent / "tags"
9
+ CSV_FILES = [ASSETS / "danbooru.csv", ASSETS / "extra-quality-tags.csv"]
10
+ gr.set_static_paths(paths=CSV_FILES)
11
+
12
+
13
+ def prompt_with_completion(*, label: str, value: str, lines: int, elem_id: str):
14
+ """Keep the standard Textbox as the value passed to generation."""
15
+ with gr.HTML(
16
+ html_template=(ASSETS / "completion.html").read_text(encoding="utf-8"),
17
+ css_template=(ASSETS / "completion.css").read_text(encoding="utf-8"),
18
+ js_on_load=(ASSETS / "completion.js").read_text(encoding="utf-8"),
19
+ csv_paths=[str(path) for path in CSV_FILES],
20
+ elem_classes=["tag-completion"],
21
+ apply_default_css=False,
22
+ padding=False,
23
+ ):
24
+ return gr.Textbox(label=label, value=value, lines=lines, elem_id=elem_id)
tags/LICENSE.tagcomplete ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Dominik Reh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tags/README.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # タグ補完用CSV
2
+
3
+ Anima / IllustriousのPrompt・Negative Promptの入力補助用データです。
4
+ `tag_completion.py`と同フォルダの`completion.html`・`completion.css`・
5
+ `completion.js`が補完UIを提供します。標準のGradio Textboxに候補リストを添える構成で、
6
+ 生成処理へ渡すPromptの文字列形式は従来どおりです。
7
+
8
+ ## 操作
9
+
10
+ - Anima / IllustriousのPositive・Negative両方で、初期状態から補完を利用できます。
11
+ - カーソル位置のタグを2文字以上入力すると、最大20件の候補を表示します。
12
+ - タグ名の前方一致、別名の前方一致、部分一致の順に、各グループ内は投稿件数順です。
13
+ - `↑`・`↓`で候補移動、`Tab` / `Enter` / クリックで確定、`Esc`で閉じます。
14
+ - `Shift+Enter`で改行します。候補を閉じた状態なら通常の`Enter`でも改行できます。
15
+ - 「タグ補完」で有効・無効、「補完設定」で表示カテゴリを切り替えられます。
16
+ - 確定時はカーソル位置のタグのみを置き換え、改行や重み指定を保持します。
17
+ - タグ名の括弧はプロンプトの重み付けと混同しないようエスケープします。
18
+ - 通常タグの`_`はスペースへ変換しますが、Animaの`score_*`・`rating_*`は保持します。
19
+ - 日本語IMEの変換中・選択範囲のある状態・LoRAやEmbeddingの指定中は補完を抑止します。
20
+
21
+ CSVは同じSpaceから初回検索時に読み込み、ブラウザ内で4欄に共有します。
22
+ タグ検索にGPU・生成キュー・外部検索APIは使いません。公開する静的ファイルは
23
+ 2つのCSVだけです。公開MCPの入力・出力は変更しません。
24
+
25
+ ## 検証
26
+
27
+ Spaceと同じGradioを用意し、リポジトリ直下で以下を実行すると、モデルをロードせず
28
+ Prompt部品の動作を確認できます。
29
+
30
+ ```text
31
+ python tests/tag_completion_demo.py
32
+ node tests/tag_completion_browser.cjs
33
+ ```
34
+
35
+ ブラウザ検証にはNode.jsの`playwright`パッケージとChromiumが必要です。
36
+ 既存Chromeを使う場合は`TEST_CHROME`に実行ファイルの絶対パスを指定します。
37
+
38
+ ## ファイル
39
+
40
+ - `danbooru.csv`: 140,782タグ、3,518,020 bytes。配布元ファイルを変更せず保存。
41
+ - `extra-quality-tags.csv`: 品質タグ6件。配布元の5列形式を、下記の4列形式へ統一。
42
+ - `LICENSE.tagcomplete`: 配布元のMITライセンスと著作権表示。
43
+
44
+ ## 形式
45
+
46
+ UTF-8、ヘッダなし。各行は以下の4列です。
47
+
48
+ ```text
49
+ tag,category,post_count,"aliases"
50
+ ```
51
+
52
+ - `tag`: 元のタグ名。スペースへの変換は入力UI側で行う想定。
53
+ - `category`: `0` 一般、`1` 作者、`3` 作品、`4` キャラクター、`5` メタ。
54
+ - `post_count`: 配布データ時点の投稿件数。品質タグは実件数がないため`0`。
55
+ - `aliases`: 別名。複数の場合は引用符内でカンマ区切り。空欄もあり。
56
+
57
+ 品質タグの投稿件数は優先順位を示しません。UIでは品質タグを一般タグと別枠で
58
+ 優先表示することを想定しています。`danbooru.csv`には作者タグも含まれるため、
59
+ カテゴリごとの表示設定を補完UI側で持たせることができます。
60
+
61
+ タグの存在や投稿件数は、そのCheckpointでの効果を保証するものではありません。
62
+ データは配布元のスナップショットであり、2026-10-14時点のDanbooru全件一覧ではありません。
63
+
64
+ ## 出典
65
+
66
+ - 元データ: Danbooruタグ。
67
+ - CSV配布元: [DominikDoom / SD WebUI Tag Autocomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete)
68
+ - 取得日: 2026-10-14(日本時間)。
69
+ - 固定リビジョン: `4170882f90b47be130a0ff9314f663c230b9153d`。
70
+ - [Danbooru CSV](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/blob/4170882f90b47be130a0ff9314f663c230b9153d/tags/danbooru.csv)
71
+ - [品質タグCSV](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/blob/4170882f90b47be130a0ff9314f663c230b9153d/tags/extra-quality-tags.csv)
72
+ - 配布元ライセンス: MIT、Copyright (c) 2022 Dominik Reh。
73
+ - `danbooru.csv` SHA-256: `f936684fa0b041e9a55d35f2052588e28d95eef8672be6829241f8b1a7214732`。
74
+
75
+ 品質タグCSVの変更点: 投稿件数欄の文字列`Quality tag`を`0`へ変更し、
76
+ 余分な5列目を除去しました。タグ名とカテゴリは維持しています。
tags/completion.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ position: relative;
2
+ .tc-tools, .tc-settings { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; }
3
+ .tc-tools { margin-top: 0.3rem; font-size: 0.85rem; color: var(--body-text-color); }
4
+ .tc-tools label { display: inline-flex; align-items: center; gap: 0.3rem; }
5
+ .tc-tools input[type="checkbox"] { appearance: auto; width: 1rem; height: 1rem; accent-color: #397ad9; }
6
+ .tc-tools summary { cursor: pointer; }
7
+ .tc-settings { padding: 0.5rem 0; }
8
+ .tc-help, .tc-status { font-size: 0.8rem; color: var(--body-text-color-subdued); }
9
+ .tc-status:empty { display: none; }
10
+ .tc-results {
11
+ position: absolute; z-index: 100; left: 0; right: 0;
12
+ max-height: 18rem; overflow-y: auto;
13
+ border: 1px solid var(--border-color-primary, #aaa); border-radius: 0.5rem;
14
+ background: var(--block-background-fill, #fff); color: var(--body-text-color, #111);
15
+ box-shadow: 0 6px 20px #0003;
16
+ }
17
+ .tc-results[hidden] { display: none; }
18
+ .tc-option {
19
+ display: flex; width: 100%; gap: 0.5rem; text-align: left; align-items: baseline;
20
+ padding: 0.4rem 0.6rem; cursor: pointer; border: 0; background: transparent;
21
+ color: inherit; font: inherit; font-size: 0.9rem;
22
+ }
23
+ .tc-option[aria-selected="true"] { background: var(--background-fill-secondary, #e7e7e7); }
24
+ .tc-name { flex: 1; overflow-wrap: anywhere; }
25
+ .tc-meta { color: var(--body-text-color-subdued, #666); font-size: 0.75rem; white-space: nowrap; }
26
+ .tc-category-0 .tc-name { color: var(--tc-general, #397ad9); }
27
+ .tc-category-1 .tc-name { color: var(--tc-artist, #cf6437); }
28
+ .tc-category-3 .tc-name { color: var(--tc-copyright, #a460c4); }
29
+ .tc-category-4 .tc-name { color: var(--tc-character, #20885a); }
30
+ .tc-category-5 .tc-name { color: var(--tc-meta, #997820); }
tags/completion.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @children
2
+ <div class="tc-tools">
3
+ <label><input class="tc-enabled" type="checkbox" checked> タグ補完</label>
4
+ <details>
5
+ <summary>補完設定</summary>
6
+ <div class="tc-settings">
7
+ <label><input type="checkbox" data-category="0" checked> 一般</label>
8
+ <label><input type="checkbox" data-category="5" checked> 品質・メタ</label>
9
+ <label><input type="checkbox" data-category="4" checked> キャラクター</label>
10
+ <label><input type="checkbox" data-category="3" checked> 作品</label>
11
+ <label><input type="checkbox" data-category="1" checked> 作者</label>
12
+ </div>
13
+ </details>
14
+ </div>
15
+ <div class="tc-help">2文字以上で候補表示 · ↑↓ 選択 · Tab / Enter 確定 · Esc 閉じる · Shift+Enter 改行</div>
16
+ <div class="tc-status" role="status" aria-live="polite"></div>
17
+ <div class="tc-results" role="listbox" aria-label="タグ候補" hidden></div>
tags/completion.js ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This script runs in Gradio HTML's js_on_load scope (element, props).
2
+ // It only attaches inside this wrapper; generation still reads its Textbox.
3
+ const normalizeTag = (value) => value.toLowerCase().replace(/_/g, " ").trim();
4
+
5
+ function parseCSV(text) {
6
+ const rows = [];
7
+ let row = [], field = "", quoted = false;
8
+ for (let i = 0; i < text.length; i++) {
9
+ const c = text[i];
10
+ if (c === '"') {
11
+ if (quoted && text[i + 1] === '"') { field += '"'; i++; }
12
+ else quoted = !quoted;
13
+ } else if (c === "," && !quoted) { row.push(field); field = ""; }
14
+ else if (c === "\n" && !quoted) {
15
+ row.push(field.replace(/\r$/, ""));
16
+ if (row.some(Boolean)) rows.push(row);
17
+ row = []; field = "";
18
+ } else field += c;
19
+ }
20
+ if (field || row.length) { row.push(field.replace(/\r$/, "")); rows.push(row); }
21
+ return rows;
22
+ }
23
+
24
+ // Share one download/parse across all four prompt fields. Use the app's root
25
+ // so the same URLs work in Spaces, standalone Gradio, and mounted applications.
26
+ const appRoot = (globalThis.gradio_config?.root || "").replace(/\/$/, "");
27
+ const urls = props.csv_paths.map(path => `${appRoot}/gradio_api/file=${encodeURIComponent(path)}`);
28
+ const cacheKey = Symbol.for("imggen.tagCompletion.v1");
29
+ const shared = globalThis[cacheKey] ||= new Map();
30
+ const datasetKey = urls.join("|");
31
+ function loadTags() {
32
+ if (!shared.has(datasetKey)) {
33
+ const pending = Promise.all(urls.map(async url => {
34
+ const response = await fetch(url);
35
+ if (!response.ok) throw new Error(`Tag CSV HTTP ${response.status}`);
36
+ return parseCSV(await response.text());
37
+ })).then(files => {
38
+ const byName = new Map();
39
+ files.forEach((rows, fileIndex) => rows.forEach(row => {
40
+ if (row.length !== 4 || !/^[01345]$/.test(row[1]) || !/^\d+$/.test(row[2])) {
41
+ throw new Error("Invalid tag CSV row");
42
+ }
43
+ const name = row[0];
44
+ const previous = byName.get(name);
45
+ byName.set(name, {
46
+ name, category: row[1], count: Math.max(Number(row[2]), previous?.count || 0),
47
+ search: normalizeTag(name),
48
+ aliases: row[3].split(",").filter(Boolean).map(normalizeTag),
49
+ quality: fileIndex > 0 || previous?.quality || false,
50
+ });
51
+ }));
52
+ return [...byName.values()].sort((a, b) => Number(b.quality) - Number(a.quality) || b.count - a.count);
53
+ }).catch(error => { shared.delete(datasetKey); throw error; });
54
+ shared.set(datasetKey, pending);
55
+ }
56
+ return shared.get(datasetKey);
57
+ }
58
+
59
+ // Find the comma/newline-delimited tag at the cursor. Keep group brackets,
60
+ // numeric weights, whitespace, and the remainder of the prompt untouched.
61
+ function currentTag(text, cursor, selectionEnd) {
62
+ if (cursor !== selectionEnd) return null;
63
+ let start = cursor, end = cursor;
64
+ while (start > 0 && !",\n".includes(text[start - 1])) start--;
65
+ while (end < text.length && !",\n".includes(text[end])) end++;
66
+ while (start < end && /\s/.test(text[start])) start++;
67
+ while (end > start && /\s/.test(text[end - 1])) end--;
68
+ while (start < end && "([".includes(text[start])) start++;
69
+ const segment = text.slice(start, end);
70
+ const weight = segment.match(/:\s*[+-]?(?:\d+(?:\.\d*)?|\.\d+)\s*[)\]]*$/);
71
+ if (weight) end = start + weight.index;
72
+ else {
73
+ // Strip only unmatched trailing group closers, not literal tag names
74
+ // such as character_(series). Escaped parentheses are literal too.
75
+ let balance = 0;
76
+ for (let i = start; i < end; i++) {
77
+ if (text[i] === "\\") { i++; continue; }
78
+ if ("([".includes(text[i])) balance++;
79
+ else if (")]".includes(text[i])) balance--;
80
+ }
81
+ while (balance < 0 && end > start && ")]".includes(text[end - 1])) { end--; balance++; }
82
+ }
83
+ while (end > start && /\s/.test(text[end - 1])) end--;
84
+ if (cursor < start || cursor > end) return null;
85
+ const query = normalizeTag(text.slice(start, cursor).replace(/\\([()\[\]])/g, "$1"));
86
+ if (query.length < 2 || /[<>:]|__|[\u3000-\u9fff]/.test(text.slice(start, cursor))) return null;
87
+ return { start, end, query };
88
+ }
89
+
90
+ const results = element.querySelector(".tc-results");
91
+ const status = element.querySelector(".tc-status");
92
+ const enabled = element.querySelector(".tc-enabled");
93
+ const allowed = () => new Set([...element.querySelectorAll("[data-category]:checked")].map(x => x.dataset.category));
94
+ let textarea, tags, choices = [], active = 0, range = null, composing = false, timer;
95
+ let request = 0;
96
+ const listId = `tc-${Math.random().toString(36).slice(2)}`;
97
+ results.id = listId;
98
+
99
+ function close() {
100
+ clearTimeout(timer); request++;
101
+ choices = []; range = null; results.hidden = true; results.replaceChildren();
102
+ textarea?.setAttribute("aria-expanded", "false");
103
+ textarea?.removeAttribute("aria-activedescendant");
104
+ }
105
+ function highlight(index) {
106
+ active = (index + choices.length) % choices.length;
107
+ [...results.children].forEach((child, i) => child.setAttribute("aria-selected", String(i === active)));
108
+ textarea.setAttribute("aria-activedescendant", `${listId}-${active}`);
109
+ results.children[active]?.scrollIntoView({ block: "nearest" });
110
+ }
111
+ function accept(index) {
112
+ if (!range || !choices[index]) return;
113
+ // Avoid inserting a stale result if a cursor move or edit occurred.
114
+ const latest = currentTag(textarea.value, textarea.selectionStart, textarea.selectionEnd);
115
+ if (!latest || latest.start !== range.start || latest.end !== range.end || latest.query !== range.query) { close(); return; }
116
+ const tagName = choices[index].name;
117
+ // Anima's score_*/rating_* control tokens must retain their underscores.
118
+ let inserted = (/^(score|rating)_/.test(tagName) ? tagName : tagName.replace(/_/g, " "))
119
+ .replace(/([()\[\]])/g, "\\$1");
120
+ const before = textarea.value.slice(0, range.start);
121
+ const after = textarea.value.slice(range.end);
122
+ if (!after.trim() && !before.match(/[(\[][^)\]]*$/)) inserted += ", ";
123
+ const caret = before.length + inserted.length;
124
+ // Native value + bubbling input updates Svelte/Gradio before Generate.
125
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value").set;
126
+ setter.call(textarea, before + inserted + after);
127
+ close();
128
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
129
+ textarea.focus();
130
+ textarea.setSelectionRange(caret, caret);
131
+ // input listeners may have scheduled another completion; cancel it.
132
+ close();
133
+ }
134
+ function render(found) {
135
+ choices = found; active = 0; results.replaceChildren();
136
+ const categories = { "0": "一般", "1": "作者", "3": "作品", "4": "キャラ", "5": "品質・メタ" };
137
+ choices.forEach((tag, index) => {
138
+ const button = document.createElement("button");
139
+ button.type = "button"; button.tabIndex = -1;
140
+ button.className = `tc-option tc-category-${tag.category}`;
141
+ button.id = `${listId}-${index}`; button.setAttribute("role", "option");
142
+ const name = document.createElement("span"); name.className = "tc-name";
143
+ name.textContent = tag.name.replace(/_/g, " ");
144
+ const meta = document.createElement("span"); meta.className = "tc-meta";
145
+ meta.textContent = `${categories[tag.category]} · ${tag.quality ? "品質タグ" : tag.count.toLocaleString()}`;
146
+ button.append(name, meta);
147
+ button.addEventListener("pointerdown", e => e.preventDefault());
148
+ button.addEventListener("click", () => accept(index));
149
+ results.append(button);
150
+ });
151
+ results.hidden = choices.length === 0;
152
+ textarea.setAttribute("aria-expanded", String(choices.length > 0));
153
+ if (choices.length) highlight(0);
154
+ }
155
+ async function update() {
156
+ const id = ++request;
157
+ const captured = currentTag(textarea.value, textarea.selectionStart, textarea.selectionEnd);
158
+ if (!enabled.checked || composing || document.activeElement !== textarea || !captured) { close(); return; }
159
+ try {
160
+ if (!tags) {
161
+ status.textContent = "タグ辞書を読み込み中…";
162
+ tags = await loadTags();
163
+ status.textContent = "";
164
+ }
165
+ if (id !== request || composing || !enabled.checked || document.activeElement !== textarea) return;
166
+ range = captured;
167
+ const categories = allowed();
168
+ const primary = [], aliases = [], partial = [];
169
+ // Dataset is already count-sorted. Prefer exact/prefix matches,
170
+ // then aliases, then substring matches; show at most 20 options.
171
+ for (const tag of tags) {
172
+ if (!categories.has(tag.category)) continue;
173
+ if (tag.search.startsWith(range.query)) { primary.push(tag); if (primary.length === 20) break; }
174
+ else if (aliases.length < 20 && tag.aliases.some(x => x.startsWith(range.query))) aliases.push(tag);
175
+ else if (partial.length < 20 && tag.search.includes(range.query)) partial.push(tag);
176
+ }
177
+ render([...primary, ...aliases, ...partial].slice(0, 20));
178
+ } catch (error) {
179
+ close(); status.textContent = "タグ辞書を読み込めませんでした。入力し直すと再試行します。";
180
+ console.error("Tag completion:", error);
181
+ }
182
+ }
183
+ function schedule() {
184
+ close();
185
+ timer = setTimeout(update, 120);
186
+ }
187
+ function attach() {
188
+ textarea = element.querySelector("textarea");
189
+ if (!textarea) return false;
190
+ textarea.setAttribute("aria-autocomplete", "list");
191
+ textarea.setAttribute("aria-controls", listId);
192
+ textarea.setAttribute("aria-expanded", "false");
193
+ textarea.addEventListener("input", schedule);
194
+ textarea.addEventListener("click", schedule);
195
+ textarea.addEventListener("focus", schedule);
196
+ textarea.addEventListener("blur", close);
197
+ textarea.addEventListener("compositionstart", () => { composing = true; close(); });
198
+ textarea.addEventListener("compositionend", () => { composing = false; schedule(); });
199
+ textarea.addEventListener("keydown", event => {
200
+ if (composing || event.isComposing || event.keyCode === 229) return;
201
+ if (event.key === "Escape") { close(); return; }
202
+ if (event.key === "Enter" && event.shiftKey && !event.ctrlKey && !event.metaKey) {
203
+ event.preventDefault(); event.stopPropagation();
204
+ textarea.setRangeText("\n", textarea.selectionStart, textarea.selectionEnd, "end");
205
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
206
+ close(); return;
207
+ }
208
+ if (!results.hidden && choices.length) {
209
+ if (["ArrowDown", "ArrowUp"].includes(event.key)) {
210
+ event.preventDefault(); highlight(active + (event.key === "ArrowDown" ? 1 : -1)); return;
211
+ }
212
+ if (!event.shiftKey && !event.ctrlKey && !event.metaKey && ["Tab", "Enter"].includes(event.key)) {
213
+ event.preventDefault(); event.stopPropagation(); accept(active); return;
214
+ }
215
+ }
216
+ if (["ArrowLeft", "ArrowRight", "Home", "End", "Tab", "Enter"].includes(event.key)) close();
217
+ });
218
+ textarea.addEventListener("keyup", event => {
219
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) schedule();
220
+ });
221
+ element.querySelectorAll(".tc-tools input").forEach(input => input.addEventListener("change", close));
222
+ return true;
223
+ }
224
+ // Children can mount after js_on_load; observe only until the textarea exists.
225
+ if (!attach()) {
226
+ const observer = new MutationObserver(() => { if (attach()) observer.disconnect(); });
227
+ observer.observe(element, { childList: true, subtree: true });
228
+ }
tags/danbooru.csv ADDED
The diff for this file is too large to render. See raw diff
 
tags/extra-quality-tags.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ masterpiece,5,0,""
2
+ best_quality,5,0,""
3
+ high_quality,5,0,""
4
+ normal_quality,5,0,""
5
+ low_quality,5,0,""
6
+ worst_quality,5,0,""
tests/admin_stats_ui.cjs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium}=require('playwright');
2
+ const assert=require('node:assert/strict');
3
+ (async()=>{
4
+ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})});
5
+ try {
6
+ for(const mobile of [false,true]) {
7
+ const page=await browser.newPage({viewport:mobile?{width:390,height:844}:{width:1440,height:1000}});
8
+ const errors=[]; page.on('pageerror',e=>errors.push(e.message));
9
+ await page.goto('http://127.0.0.1:7869');
10
+ const toggle=page.getByRole('button',{name:'利用状況・生成統計'});
11
+ await toggle.click();
12
+ await page.getByText('PRIVATE-TEST-MODEL',{exact:true}).filter({visible:true}).first().waitFor();
13
+ assert.equal(await page.getByRole('button',{name:'Hugging Faceでログイン'}).count(),0);
14
+ const text=await page.locator('body').innerText();
15
+ for(const label of ['昨日・今日のモデル別生成件数','月別合計','統計ファイル更新日時','ログイン不要']) assert(text.includes(label));
16
+ await page.getByRole('button',{name:'生成統計を更新'}).click();
17
+ await page.getByText('PRIVATE-TEST-MODEL',{exact:true}).filter({visible:true}).first().waitFor();
18
+ const config=await (await page.request.get('http://127.0.0.1:7869/config')).json();
19
+ const events=config.dependencies.filter(d=>d.api_name?.startsWith('refresh_statistics'));
20
+ assert(events.length>=1); assert(events.every(d=>d.queue===false&&d.api_visibility==='private'));
21
+ assert.deepEqual(errors,[]);
22
+ await page.close();
23
+ console.log('PASS '+(mobile?'mobile':'desktop')+' anonymous public statistics and refresh');
24
+ }
25
+ } finally {await browser.close();}
26
+ })().catch(e=>{console.error(e);process.exit(1);});
tests/admin_stats_ui_demo.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LOOPBACK TEST ONLY: public reports with synthetic non-user statistics."""
2
+ import json
3
+ import sys
4
+ import tempfile
5
+ from datetime import datetime, timedelta
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ import gradio as gr
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
+ import admin_stats
13
+ from generation_stats import JST
14
+
15
+
16
+ if __name__ == "__main__":
17
+ today = datetime.now(JST).date()
18
+ yesterday = today - timedelta(days=1)
19
+ with tempfile.TemporaryDirectory(prefix="admin-stats-test-") as directory:
20
+ path = Path(directory) / "daily-counts.json"
21
+ path.write_text(json.dumps({"version": 1, "timezone": "Asia/Tokyo", "counts": {
22
+ yesterday.isoformat(): {"anima": {"PRIVATE-TEST-MODEL": 26}},
23
+ today.isoformat(): {"anima": {"PRIVATE-TEST-MODEL": 94}, "illustrious": {"ILL-TEST": 2}},
24
+ }}), encoding="utf-8")
25
+ with gr.Blocks(analytics_enabled=False) as demo:
26
+ admin_stats.build_admin_statistics(demo, SimpleNamespace(path=path), {"anima": ["PRIVATE-TEST-MODEL"], "illustrious": ["ILL-TEST", "UNUSED"]})
27
+ demo.launch(server_name="127.0.0.1", server_port=7869)
tests/browser_settings_ui.cjs ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium} = require('playwright');
2
+ const assert = require('node:assert/strict');
3
+ const key = family => `image-model-collection:settings:v1:${family}`;
4
+
5
+ (async () => {
6
+ const browser = await chromium.launch({headless:true, ...(process.env.TEST_CHROME ? {executablePath:process.env.TEST_CHROME} : {})});
7
+ try {
8
+ const page = await browser.newPage({viewport:{width:1360,height:1000}});
9
+ const errors = [];
10
+ page.on('pageerror', error => errors.push(error.message));
11
+ const number = label => page.getByRole('spinbutton', {name:`number input for ${label}`, exact:true});
12
+ const stored = family => page.evaluate(k => JSON.parse(localStorage.getItem(k)), key(family));
13
+ const saved = (family, field, value) => page.waitForFunction(({key,field,value}) => {
14
+ const data = JSON.parse(localStorage.getItem(key));
15
+ return data?.values?.[field] === value;
16
+ }, {key:key(family),field,value});
17
+ async function setNumber(label, value) {
18
+ await number(label).fill(String(value)); await number(label).press('Tab');
19
+ }
20
+ async function choose(label, value) {
21
+ await page.getByRole('combobox',{name:label,exact:true}).fill(value);
22
+ await page.getByRole('option').filter({hasText:value}).first().click();
23
+ }
24
+ async function settings() {
25
+ const area = page.locator('.bs-settings').filter({visible:true});
26
+ if (!await area.evaluate(el => el.open)) await area.locator('summary').click();
27
+ return area;
28
+ }
29
+ await page.goto('http://127.0.0.1:7868');
30
+ await saved('anima','cfg',5);
31
+ assert.equal(await page.locator('.bs-settings').first().evaluate(el => el.open),false);
32
+ let area = await settings();
33
+ assert.equal(await area.locator('.bs-enabled').isChecked(),true);
34
+ assert.equal(await area.locator('.bs-prompts').isChecked(),true);
35
+ await page.getByRole('textbox',{name:'Prompt',exact:true}).fill('saved by default');
36
+ await page.getByRole('textbox',{name:'Negative prompt',exact:true}).fill('negative saved by default');
37
+ await saved('anima','prompt','saved by default');
38
+ await saved('anima','negative_prompt','negative saved by default');
39
+ await page.reload();
40
+ await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
41
+ assert.equal(await page.getByRole('textbox',{name:'Prompt',exact:true}).inputValue(),'saved by default');
42
+ assert.equal(await page.getByRole('textbox',{name:'Negative prompt',exact:true}).inputValue(),'negative saved by default');
43
+ area = await settings();
44
+ await area.locator('.bs-prompts').uncheck();
45
+ console.log('PASS prompts saved by default and both prompts restored');
46
+ await page.getByRole('textbox',{name:'Prompt',exact:true}).fill('private unsaved prompt');
47
+ await page.getByRole('radio',{name:'非Turbo+Turbo LoRA',exact:true}).check();
48
+ await saved('anima','cfg',1);
49
+ await setNumber('CFG',3.2);
50
+ await setNumber('Steps',23);
51
+ await setNumber('Width',1152);
52
+ await choose('Anima Checkpoint','anima-checkpoint-2');
53
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
54
+ await choose('LoRA 3 File','existing.safetensors');
55
+ await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).check();
56
+ await saved('anima','lora3_enabled',true);
57
+ await saved('anima','cfg',3.2);
58
+ assert.equal(Object.hasOwn((await stored('anima')).values,'prompt'),false);
59
+ await page.reload();
60
+ await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
61
+ await saved('anima','cfg',3.2);
62
+ assert.equal(await number('CFG').inputValue(),'3.2');
63
+ assert.equal(await number('Steps').inputValue(),'23');
64
+ assert.equal(await number('Width').inputValue(),'1152');
65
+ assert.equal(await page.getByRole('radio',{name:'非Turbo+Turbo LoRA',exact:true}).isChecked(),true);
66
+ assert.equal(await page.getByRole('textbox',{name:'Prompt',exact:true}).inputValue(),'masterpiece, best quality,');
67
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
68
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),true);
69
+ console.log('PASS collapsed auto-restore, custom sampling/dimensions preserved, LoRA enabled, prompts excluded');
70
+
71
+ area = await settings();
72
+ await area.locator('.bs-prompts').check();
73
+ await page.getByRole('textbox',{name:'Prompt',exact:true}).fill('<img src=x onerror=alert(1)> saved as plain text');
74
+ await saved('anima','prompt','<img src=x onerror=alert(1)> saved as plain text');
75
+ await page.reload();
76
+ await page.waitForFunction(() => document.querySelector('#anima-positive-prompt textarea')?.value.includes('saved as plain text'));
77
+ assert.equal(await page.locator('#anima-positive-prompt img').count(),0);
78
+ area = await settings();
79
+ await area.locator('.bs-prompts').uncheck();
80
+ assert.equal(Object.hasOwn((await stored('anima')).values,'prompt'),false);
81
+ await page.reload();
82
+ await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
83
+ area = await settings();
84
+ assert.equal(await area.locator('.bs-prompts').isChecked(),false);
85
+ console.log('PASS prompt opt-in, plain-text restoration, opt-out removes stored prompt');
86
+
87
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
88
+ await saved('illustrious','cfg',5);
89
+ await choose('VAE','SDXL VAE');
90
+ await page.getByRole('checkbox',{name:'test-embedding',exact:true}).check();
91
+ await setNumber('CFG',4.1);
92
+ await saved('illustrious','vae','sdxl');
93
+ await saved('illustrious','cfg',4.1);
94
+ assert.equal((await stored('anima')).values.cfg,3.2);
95
+ await page.reload();
96
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
97
+ await page.waitForFunction(() => [...document.querySelectorAll('input[aria-label="number input for CFG"]')].some(el => el.value==='4.1'));
98
+ assert.equal(await number('CFG').inputValue(),'4.1');
99
+ assert.equal(await page.getByRole('combobox',{name:'VAE',exact:true}).inputValue(),'SDXL VAE');
100
+ assert.equal(await page.getByRole('checkbox',{name:'test-embedding',exact:true}).isChecked(),true);
101
+ console.log('PASS independent family settings and Illustrious VAE');
102
+
103
+ area = await settings();
104
+ await area.locator('.bs-reset').click();
105
+ await saved('illustrious','cfg',5);
106
+ assert.equal(await number('CFG').inputValue(),'5');
107
+ await area.locator('.bs-delete').click();
108
+ let record = await stored('illustrious');
109
+ assert.deepEqual(record,{version:1,family:'illustrious',enabled:false});
110
+ await setNumber('CFG',6);
111
+ await page.waitForTimeout(900);
112
+ assert.equal((await stored('illustrious')).values,undefined);
113
+ await page.reload();
114
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
115
+ area = await settings();
116
+ await page.waitForFunction(() => [...document.querySelectorAll('.bs-enabled')].some(el => !el.disabled&&!el.checked));
117
+ assert.equal(await area.locator('.bs-enabled').isChecked(),false);
118
+ assert.equal(await number('CFG').inputValue(),'5');
119
+ console.log('PASS reset defaults, deletion, disabled saving stays disabled on reload');
120
+
121
+ // Inject before app initialization; pagehide normally saves the current UI.
122
+ await page.addInitScript(k => localStorage.setItem(k, JSON.stringify({version:1,family:'anima',enabled:true,prompts:false,values:{model:'../evil',cfg:999,width:99999,lora3_file:'../evil.safetensors',lora3_enabled:true,prompt:'must not restore'}})), key('anima'));
123
+ await page.reload();
124
+ await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
125
+ await saved('anima','cfg',5);
126
+ record = await stored('anima');
127
+ assert.equal(record.values.model,'anima-checkpoint');
128
+ assert.equal(record.values.lora3_file,'---');
129
+ assert.equal(record.values.lora3_enabled,false);
130
+ assert.equal(Object.hasOwn(record.values,'prompt'),false);
131
+ assert.equal(await page.getByText('Received LoRA slots:',{exact:false}).count(),0);
132
+ console.log('PASS modified browser data validated and restoration never generates');
133
+
134
+ const mobile = await browser.newPage({viewport:{width:390,height:844},isMobile:true,hasTouch:true});
135
+ mobile.on('pageerror', error => errors.push(error.message));
136
+ await mobile.goto('http://127.0.0.1:7868');
137
+ await mobile.locator('.bs-settings summary').click();
138
+ await mobile.locator('.bs-enabled:not(:disabled)').waitFor();
139
+ assert.equal(await mobile.evaluate(() => document.documentElement.scrollWidth<=innerWidth),true);
140
+ assert.equal(await mobile.locator('.bs-prompts').isChecked(),true);
141
+ await mobile.locator('.bs-prompts').check();
142
+ await mobile.getByRole('textbox',{name:'Prompt',exact:true}).fill('mobile prompt');
143
+ await mobile.waitForFunction(k => JSON.parse(localStorage.getItem(k))?.values?.prompt==='mobile prompt',key('anima'));
144
+ await mobile.reload();
145
+ await mobile.waitForFunction(() => document.querySelector('#anima-positive-prompt textarea')?.value==='mobile prompt');
146
+ console.log('PASS mobile layout and restoration');
147
+
148
+ const blocked = await browser.newPage();
149
+ blocked.on('pageerror', error => errors.push(error.message));
150
+ await blocked.addInitScript(() => {
151
+ const get = Storage.prototype.getItem, set = Storage.prototype.setItem;
152
+ Storage.prototype.getItem = function(key) {
153
+ if (key.startsWith('image-model-collection:settings:')) throw new Error('storage denied');
154
+ return get.call(this,key);
155
+ };
156
+ Storage.prototype.setItem = function(key,value) {
157
+ if (key.startsWith('image-model-collection:settings:')) throw new Error('storage denied');
158
+ return set.call(this,key,value);
159
+ };
160
+ });
161
+ await blocked.goto('http://127.0.0.1:7868');
162
+ await blocked.locator('.bs-settings summary').click();
163
+ await blocked.getByText('ブラウザの保存領域を利用できません。現在の設定で生成はできます。',{exact:true}).waitFor();
164
+ assert.equal(await blocked.getByRole('button',{name:'Generate with Anima',exact:true}).isEnabled(),true);
165
+ console.log('PASS unavailable browser storage does not prevent generation');
166
+ assert.deepEqual(errors,[]);
167
+ } finally { await browser.close(); }
168
+ })().catch(error => {console.error(error);process.exitCode=1;});
tests/eye_type_samples_ui.cjs ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium} = require('playwright');
2
+ const assert = require('node:assert/strict');
3
+ const cases = [
4
+ ['hotarueye_xl_comic01_v20_ill','sdxl/hotarueye_xl_comic01_v20.webp',1,null],
5
+ ['hotarueye_xl_comic04_v10_ill','sdxl/hotarueye_xl_comic04_v10.webp',4,null],
6
+ ...Array.from({length:6},(_,index)=>['hotarupupil_xl_type'+(index+1)+'_v10','sdxl/pupil/v10_sample1.webp',index+1,[768+(index%3)*400,Math.floor(index/3)*400]]),
7
+ ['hotarueye_xl_hosome1_v30','sdxl/hosome/type1_v30.webp',1,null],
8
+ ['hotarueye_xl_hosome2_v30','sdxl/hosome/type2_v30.webp',2,null],
9
+ ];
10
+
11
+ async function assertViewport(locator, xy) {
12
+ const geometry = await locator.evaluate(el=>{
13
+ const frame=el.closest('.jb-crop-frame').getBoundingClientRect(), image=el.getBoundingClientRect();
14
+ return {x:(frame.x-image.x)/frame.width*400,y:(frame.y-image.y)/frame.height*400,
15
+ sourceWidth:el.naturalWidth,sourceHeight:el.naturalHeight};
16
+ });
17
+ assert(Math.abs(geometry.x-xy[0])<.1,JSON.stringify(geometry));
18
+ assert(Math.abs(geometry.y-xy[1])<.1,JSON.stringify(geometry));
19
+ assert.equal(geometry.sourceWidth,1968);assert.equal(geometry.sourceHeight,800);
20
+ }
21
+
22
+ (async()=>{
23
+ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})});
24
+ try {
25
+ const errors=[];
26
+ for(const mobile of [false,true]) {
27
+ const page=await browser.newPage({viewport:mobile?{width:390,height:844}:{width:1440,height:1000},isMobile:mobile,hasTouch:mobile});
28
+ page.on('pageerror',e=>errors.push(e.message));
29
+ await page.goto('http://127.0.0.1:7868');
30
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
31
+ await page.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click();
32
+ const area=page.locator('.jujo-samples:visible');
33
+ for(const [name,image,type,xy] of cases) {
34
+ await area.locator('.jb-search').fill(name);
35
+ assert.equal(await area.locator('.jb-card').count(),1);
36
+ const card=area.locator('.jb-card');
37
+ assert((await card.locator('img').getAttribute('src')).endsWith('/'+image));
38
+ assert.match(await card.locator('.jb-code').innerText(),new RegExp('Type '+type));
39
+ await card.click();
40
+ await area.locator('.jb-preview img').evaluate(img=>img.complete&&img.naturalWidth>0?true:new Promise((resolve,reject)=>{img.onload=resolve;img.onerror=reject;}));
41
+ assert.equal(await area.locator('.jb-preview img').getAttribute('src'),await card.locator('img').getAttribute('src'));
42
+ assert.match(await area.locator('.jb-sample-note').innerText(),new RegExp('Type '+type));
43
+ if(xy) {
44
+ await assertViewport(card.locator('img'),xy);
45
+ await assertViewport(area.locator('.jb-preview img'),xy);
46
+ assert.equal(await area.locator('.jb-original-source').getAttribute('href'),await card.locator('img').getAttribute('src'));
47
+ }
48
+ await area.locator('.jb-preview').click();
49
+ assert.equal(await area.locator('.jb-zoom').evaluate(el=>el.matches(':modal')),true);
50
+ await area.locator('.jb-zoom img').evaluate(img=>img.complete&&img.naturalWidth>0?true:new Promise((resolve,reject)=>{img.onload=resolve;img.onerror=reject;}));
51
+ if(xy) await assertViewport(area.locator('.jb-zoom img'),xy);
52
+ await area.locator('.jb-zoom-close').click();
53
+ const screenshot=mobile?process.env.EYE_TYPES_MOBILE_SCREENSHOT:process.env.EYE_TYPES_DESKTOP_SCREENSHOT;
54
+ if(screenshot&&type===6&&xy) {
55
+ if(mobile) {await area.locator('.jb-detail').evaluate(el=>{el.scrollTop=0;});await page.screenshot({path:screenshot});}
56
+ else await area.screenshot({path:screenshot});
57
+ }
58
+ if(mobile) await area.locator('.jb-close').click();
59
+ }
60
+ await area.locator('.jb-search').fill('hotarupupil_xl_type6_v10');
61
+ await area.locator('.jb-card').click();
62
+ await area.locator('.jb-target').selectOption('3');
63
+ await area.locator('.jb-apply-button').click();
64
+ await area.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました'}).waitFor();
65
+ if(mobile) await area.locator('.jb-close').click();
66
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
67
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),true);
68
+ assert.match(await page.getByRole('combobox',{name:'LoRA 3 File',exact:true}).inputValue(),/hotarupupil_xl_type6_v10/);
69
+ assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);
70
+ console.log('PASS',mobile?'mobile':'desktop','all 10 type samples, exact pupil framing in cards/details/zoom, original link and slot assignment');
71
+ await page.close();
72
+ }
73
+ assert.deepEqual(errors,[]);
74
+ }finally{await browser.close();}
75
+ })().catch(e=>{console.error(e);process.exitCode=1;});
tests/eyecolle_samples_ui.cjs ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium} = require('playwright');
2
+ const assert = require('node:assert/strict');
3
+
4
+ (async () => {
5
+ const browser = await chromium.launch({headless:true, ...(process.env.TEST_CHROME ? {executablePath:process.env.TEST_CHROME} : {})});
6
+ try {
7
+ const errors = [];
8
+ for (const mobile of [false,true]) {
9
+ const page = await browser.newPage({viewport: mobile ? {width:390,height:844} : {width:1440,height:1000}, isMobile:mobile, hasTouch:mobile});
10
+ page.on('pageerror',error=>errors.push(error.message));
11
+ await page.goto('http://127.0.0.1:7868');
12
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
13
+ await page.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click();
14
+ const area = page.locator('.jujo-samples:visible');
15
+ for (const [code,shape] of [['127','tareme'],['115','tsurime']]) {
16
+ await area.locator('.jb-search').fill('eyecolle_xl_code'+code);
17
+ assert.equal(await area.locator('.jb-card').count(),1);
18
+ const card = area.locator('.jb-card');
19
+ assert.equal(await card.locator('.jb-code').innerText(),'code'+code);
20
+ assert.match(await card.locator('img').getAttribute('src'),new RegExp('/'+shape+'_ill'+code+'\\.webp$'));
21
+ await card.click();
22
+ const preview = area.locator('.jb-preview img');
23
+ assert.equal(await preview.getAttribute('src'),await card.locator('img').getAttribute('src'));
24
+ await preview.evaluate(img=>img.complete&&img.naturalWidth>0 ? true : new Promise((resolve,reject)=>{img.addEventListener('load',resolve,{once:true});img.addEventListener('error',reject,{once:true});}));
25
+ assert.match(await area.locator('.jb-sample-note').innerText(),new RegExp('code'+code+'.*Illustrious'));
26
+ assert.match(await area.locator('.jb-hint').innerText(),new RegExp(shape));
27
+ assert.equal(await area.locator('.jb-strength').inputValue(),'1.5');
28
+ assert.match(await area.locator('.jb-source').getAttribute('href'),/JujoHotaru\/lora\/blob\/9e51c3d49a291408c00f15a04d903948b819cc4d\/sdxl\/eyecollexl\/README.md$/);
29
+ await area.locator('.jb-preview').click();
30
+ assert.equal(await area.locator('.jb-zoom img').getAttribute('src'),await preview.getAttribute('src'));
31
+ assert.equal(await area.locator('.jb-zoom').evaluate(el=>el.matches(':modal')),true);
32
+ await area.locator('.jb-zoom-close').click();
33
+ if (mobile) await area.locator('.jb-close').click();
34
+ }
35
+ if (mobile) await area.locator('.jb-card').click();
36
+ await area.locator('.jb-target').selectOption('3');
37
+ await area.locator('.jb-apply-button').click();
38
+ await area.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました'}).waitFor();
39
+ if (mobile) await area.locator('.jb-close').click();
40
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
41
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),true);
42
+ assert.match(await page.getByRole('combobox',{name:'LoRA 3 File',exact:true}).inputValue(),/eyecolle_xl_code115/);
43
+ if (mobile) await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
44
+ if (mobile) await area.locator('.jb-card').click();
45
+ const screenshot = mobile ? process.env.EYECOLLE_MOBILE_SCREENSHOT : process.env.EYECOLLE_DESKTOP_SCREENSHOT;
46
+ if (screenshot) {
47
+ if (mobile) {
48
+ await area.locator('.jb-detail').evaluate(el=>{el.scrollTop=0;});
49
+ await page.screenshot({path:screenshot});
50
+ } else await area.screenshot({path:screenshot});
51
+ }
52
+ assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);
53
+ console.log('PASS',mobile?'mobile':'desktop','exact code thumbnail/detail/zoom, ILL images loaded, shape hints and slot assignment');
54
+ await page.close();
55
+ }
56
+ assert.deepEqual(errors,[]);
57
+ } finally {await browser.close();}
58
+ })().catch(error=>{console.error(error);process.exitCode=1;});
tests/favorites_ui.cjs ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium}=require('playwright');
2
+ const assert=require('node:assert/strict');
3
+ const favoriteKey=f=>`image-model-collection:favorites:v1:${f}`;
4
+ const settingsKey=f=>`image-model-collection:settings:v1:${f}`;
5
+ (async()=>{
6
+ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})});
7
+ try {
8
+ for(const mobile of [false,true]) {
9
+ const page=await browser.newPage({viewport:mobile?{width:390,height:844}:{width:1360,height:1000},isMobile:mobile,hasTouch:mobile,acceptDownloads:true});
10
+ const errors=[]; page.on('pageerror',e=>errors.push(e.message));
11
+ let accept=true; page.on('dialog',d=>accept?d.accept():d.dismiss());
12
+ await page.goto('http://127.0.0.1:7868');
13
+ const panel=page.locator('.bf-panel:visible');
14
+ const prompt=page.getByRole('textbox',{name:'Prompt',exact:true});
15
+ const number=label=>page.getByRole('spinbutton',{name:`number input for ${label}`,exact:true});
16
+ const data=()=>page.evaluate(k=>JSON.parse(localStorage.getItem(k)),favoriteKey('anima'));
17
+ const count=async n=>{await page.waitForFunction(({k,n})=>JSON.parse(localStorage.getItem(k))?.entries.length===n,{k:favoriteKey('anima'),n});};
18
+ const importData=async object=>panel.locator('.bf-file').setInputFiles({name:'favorites.json',mimeType:'application/json',buffer:Buffer.from(JSON.stringify(object))});
19
+ await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
20
+ assert.equal(await panel.evaluate(el=>el.open),false);
21
+ assert(await page.evaluate(()=>{
22
+ const visible=selector=>[...document.querySelectorAll(selector)].find(el=>el.offsetParent!==null);
23
+ const lora=visible('.lora-accordion'), samples=visible('.jujo-samples');
24
+ const generate=[...document.querySelectorAll('button')].find(el=>el.textContent.trim()==='Generate with Anima');
25
+ return Boolean(lora.compareDocumentPosition(samples)&Node.DOCUMENT_POSITION_FOLLOWING)&&Boolean(samples.compareDocumentPosition(generate)&Node.DOCUMENT_POSITION_FOLLOWING);
26
+ }),'LoRA -> JujoHotaru -> Generate order');
27
+ await panel.locator('summary').click();
28
+ assert.equal(await panel.locator('.bf-apply').innerText(),'設定を読み込む');
29
+ assert.match(await panel.innerText(),/サイトデータの削除/);
30
+ assert.match(await panel.innerText(),/画像生成は開始しません/);
31
+ assert.match(await page.locator('.generation-count-notice:visible').first().innerText(),/モデルの入れ替え・ラインナップ改善/);
32
+ assert.match(await page.locator('.generation-count-notice:visible').first().innerText(),/利用者を識別する情報は含みません/);
33
+ await panel.locator('.bf-name').fill('<b>水彩</b>');
34
+ await prompt.fill('<img src=x onerror=alert(1)> favorite prompt');
35
+ await number('CFG').fill('3.2'); await number('CFG').press('Tab');
36
+ await panel.locator('.bf-add').click(); await count(1);
37
+ let first=(await data()).entries[0];
38
+ assert.equal(first.values.prompt,'<img src=x onerror=alert(1)> favorite prompt');
39
+ assert.equal(first.values.cfg,3.2); assert.equal(first.values.seed,'-1');
40
+ assert.equal(await panel.locator('b,img').count(),0);
41
+ await prompt.fill('changed'); await panel.locator('.bf-apply').click();
42
+ await panel.locator('.bf-message').filter({hasText:'設定を読み込みました'}).waitFor();
43
+ assert.equal(await prompt.inputValue(),first.values.prompt);
44
+ await prompt.fill('updated'); accept=false; await panel.locator('.bf-update').click();
45
+ assert.equal((await data()).entries[0].values.prompt,first.values.prompt);
46
+ accept=true; await panel.locator('.bf-update').click();
47
+ await page.waitForFunction(k=>JSON.parse(localStorage.getItem(k)).entries[0].values.prompt==='updated',favoriteKey('anima'));
48
+ const downloadPromise=page.waitForEvent('download'); await panel.locator('.bf-export').click();
49
+ const download=await downloadPromise; const stream=await download.createReadStream(); const chunks=[];for await(const chunk of stream)chunks.push(chunk);
50
+ assert.equal(download.suggestedFilename(),'anima-collection-plus-mcp-favorites-anima.json');
51
+ const exported=JSON.parse(Buffer.concat(chunks)); assert.equal(exported.entries.length,1);
52
+ assert.equal(exported.format,'anima-collection-plus-mcp-favorites');
53
+ await importData({...exported,format:'imggen-favorites'}); await count(2);
54
+ assert.equal((await data()).entries[0].name,(await data()).entries[1].name);
55
+ await panel.locator('.bf-delete').click(); await count(1);
56
+ await page.reload(); await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
57
+ await panel.locator('summary').click(); assert.equal(await panel.locator('.bf-list option').count(),2);
58
+ await panel.locator('.bf-list').selectOption({index:1});
59
+ await panel.locator('.bf-seed').selectOption('last'); await panel.locator('.bf-name').fill('last seed');
60
+ await panel.locator('.bf-add').click(); await panel.locator('.bf-message').filter({hasText:'まだ生成が完了していません'}).waitFor();
61
+ await page.getByRole('button',{name:'Generate with Anima',exact:true}).click();
62
+ await page.getByText(/Received LoRA slots:.*seed 3952492001372691585/).waitFor();
63
+ await panel.locator('.bf-add').click(); await count(2);
64
+ assert.equal((await data()).entries[1].values.seed,'3952492001372691585');
65
+ await panel.locator('.bf-apply').click(); await panel.locator('.bf-message').filter({hasText:'設定を読み込みました'}).waitFor();
66
+ await page.getByRole('button',{name:'Generate with Anima',exact:true}).click();
67
+ await page.getByText(/Received LoRA slots:.*seed 3952492001372691585/).waitFor();
68
+ const actualSeed=page.locator('input[aria-label*="Seed"]:visible');
69
+ await actualSeed.fill('42'); await actualSeed.press('Tab');
70
+ await page.waitForTimeout(500);
71
+ await page.getByRole('button',{name:'Generate with Anima',exact:true}).click();
72
+ await page.getByText(/Received LoRA slots:.*seed 42$/).waitFor();
73
+ // Automatic prompt opt-out does not silently wipe explicit favorites.
74
+ const settings=page.locator('.bs-settings:visible'); await settings.locator('summary').click();
75
+ await settings.locator('.bs-prompts').uncheck(); await settings.locator('.bs-enabled').uncheck();
76
+ assert.equal((await data()).entries.length,2);
77
+ await panel.locator('.bf-seed').selectOption('input'); await panel.locator('.bf-name').fill('explicit');
78
+ await prompt.fill('explicit while auto save off'); await panel.locator('.bf-add').click(); await count(3);
79
+ assert.equal((await data()).entries[2].values.prompt,'explicit while auto save off');
80
+ // Reject malformed/wrong-family/range/oversized imports atomically.
81
+ for(const invalid of [{...exported,family:'illustrious'}, {...exported,entries:[{name:'x',values:{cfg:999}}]}, {...exported,entries:Array(101).fill(exported.entries[0])}]) {
82
+ await importData(invalid); await panel.locator('.bf-message').filter({hasText:'インポートできません'}).waitFor(); assert.equal((await data()).entries.length,3);
83
+ }
84
+ const missing={...exported,entries:[{name:'missing LoRA',values:{...first.values,lora3_file:'removed.safetensors',lora3_enabled:true,unexpected:'not stored'}}]};
85
+ await importData(missing); await count(4); assert(!Object.hasOwn((await data()).entries[3].values,'unexpected'));
86
+ await panel.locator('.bf-list').selectOption({index:4}); await panel.locator('.bf-apply').click();
87
+ await panel.locator('.bf-message').filter({hasText:'未登録の項目'}).waitFor();
88
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
89
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),false);
90
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
91
+ await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
92
+ await panel.locator('summary').click(); assert.equal(await panel.locator('.bf-list option').count(),1);
93
+ await panel.locator('.bf-name').fill('ILL'); await panel.locator('.bf-add').click();
94
+ await page.waitForFunction(k=>JSON.parse(localStorage.getItem(k))?.entries.length===1,favoriteKey('illustrious'));
95
+ await page.getByRole('tab',{name:'Anima',exact:true}).click();
96
+ const beforePrompt=await prompt.inputValue(); const beforeAuto=await page.evaluate(k=>localStorage.getItem(k),settingsKey('anima'));
97
+ accept=false; await panel.locator('.bf-clear').click(); assert.equal((await data()).entries.length,4);
98
+ accept=true; await panel.locator('.bf-clear').click();
99
+ assert.equal(await data(),null); assert.equal(await prompt.inputValue(),beforePrompt);
100
+ assert.equal(await page.evaluate(k=>localStorage.getItem(k),settingsKey('anima')),beforeAuto);
101
+ assert.equal(await page.evaluate(k=>JSON.parse(localStorage.getItem(k)).entries.length,favoriteKey('illustrious')),1);
102
+ await importData({...exported,entries:Array.from({length:100},(_,i)=>({...exported.entries[0],name:(i<50?'水彩ABC ':'油彩xyz ')+i}))}); await count(100);
103
+ assert(await panel.locator('.bf-add').isDisabled());
104
+ await importData(exported); await panel.locator('.bf-message').filter({hasText:'100件を超えます'}).waitFor(); await count(100);
105
+ await page.evaluate(()=>{const original=Storage.prototype.setItem; window.restoreStorage=()=>Storage.prototype.setItem=original;Storage.prototype.setItem=function(){throw Error('quota test');};});
106
+ await panel.locator('.bf-list').selectOption({index:1}); await panel.locator('.bf-update').click();
107
+ await panel.locator('.bf-message').filter({hasText:'失敗しました'}).waitFor(); await count(100);
108
+ await page.evaluate(()=>window.restoreStorage());
109
+ await panel.locator('.bf-search').fill('水彩abc');
110
+ assert.equal(await panel.locator('.bf-list option').count(),51);
111
+ assert.match(await panel.locator('.bf-count').innerText(),/50件表示/全100件/);
112
+ await panel.locator('.bf-list').selectOption({index:1});
113
+ assert(!await panel.locator('.bf-apply').isDisabled());
114
+ await panel.locator('.bf-search').fill('油彩XYZ');
115
+ assert.equal(await panel.locator('.bf-list').inputValue(),'');
116
+ assert(await panel.locator('.bf-update').isDisabled()); assert(await panel.locator('.bf-delete').isDisabled());
117
+ assert.equal(await panel.locator('.bf-name').inputValue(),'');
118
+ await panel.locator('.bf-search').fill('not found');
119
+ assert.equal(await panel.locator('.bf-list option').count(),1);
120
+ assert.match(await panel.locator('.bf-list').innerText(),/一致するお気に入りがありません/);
121
+ await count(100);
122
+ const allDownloadPromise=page.waitForEvent('download'); await panel.locator('.bf-export').click();
123
+ const allDownload=await allDownloadPromise; const allStream=await allDownload.createReadStream(); const allChunks=[];
124
+ for await(const chunk of allStream)allChunks.push(chunk);
125
+ assert.equal(JSON.parse(Buffer.concat(allChunks)).entries.length,100);
126
+ await panel.locator('.bf-search').fill(''); assert.equal(await panel.locator('.bf-list option').count(),101);
127
+ await panel.locator('.bf-search').fill('水彩');
128
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click(); assert.equal(await panel.locator('.bf-search').inputValue(),'');
129
+ await page.getByRole('tab',{name:'Anima',exact:true}).click();
130
+ await panel.locator('.bf-clear').click(); assert.equal(await data(),null);
131
+ assert.equal(await page.evaluate(k=>JSON.parse(localStorage.getItem(k)).entries.length,favoriteKey('illustrious')),1);
132
+ assert(await page.evaluate(()=>document.documentElement.scrollWidth <= innerWidth+2));
133
+ assert.deepEqual(errors,[]); await page.close();
134
+ console.log(`PASS ${mobile?'mobile':'desktop'} 100 favorites, name search, filtered full export/clear, CRUD, JSON, exact seed and mode isolation`);
135
+ }
136
+ } finally {await browser.close();}
137
+ })().catch(e=>{console.error(e);process.exit(1);});
tests/jujo_browser_ui.cjs ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium}=require('playwright');
2
+ const assert=require('node:assert/strict');
3
+
4
+ (async()=>{
5
+ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})});
6
+ try {
7
+ const page=await browser.newPage({viewport:{width:1440,height:1000}});
8
+ const errors=[];page.on('pageerror',e=>errors.push(e.message));
9
+ await page.goto('http://127.0.0.1:7868',{waitUntil:'domcontentloaded'});
10
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
11
+ const openSamples=()=>page.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click();
12
+ await openSamples();
13
+ let area=page.locator('.jujo-samples:visible');
14
+ await area.locator('.jb-card').first().waitFor();
15
+ assert.equal(await area.locator('.jb-card').count(),8);
16
+ await area.locator('.jb-category-tabs').getByRole('button',{name:'目の形',exact:true}).click();
17
+ await area.locator('.jb-search').fill('eye126A');
18
+ assert.equal(await area.locator('.jb-card').count(),1);
19
+ await area.locator('.jb-card').click();
20
+ assert.equal(await area.locator('.jb-target').inputValue(),'3');
21
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true,includeHidden:true}).isChecked(),false);
22
+ await area.locator('.jb-strength').fill('0.9');
23
+ await area.locator('.jb-apply-button').click();
24
+ await area.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました'}).waitFor();
25
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true,includeHidden:true}).isChecked(),true);
26
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 2 Enable',exact:true,includeHidden:true}).isChecked(),false);
27
+ await page.getByRole('button',{name:'Generate with Anima',exact:true}).click();
28
+ await page.getByText('Received LoRA slots:',{exact:false}).filter({hasText:'eye126A_anima_v10.safetensors'}).waitFor();
29
+ console.log('PASS desktop 8 cards, search/category, empty-slot priority, explicit apply, other slots preserved');
30
+
31
+ await area.locator('.jb-search').fill('');
32
+ await area.locator('.jb-next').click();
33
+ assert.match(await area.locator('.jb-page').innerText(),/^2 \/ /);
34
+ await area.locator('.jb-card').first().click();
35
+ assert.equal(await area.locator('.jb-target').inputValue(),'2');
36
+ console.log('PASS pagination and disabled-slot fallback');
37
+
38
+ await page.getByRole('checkbox',{name:'LoRA 2 Enable',exact:true}).check();
39
+ await page.getByRole('checkbox',{name:'LoRA 4 Enable',exact:true}).check();
40
+ await page.waitForFunction(()=>[...document.querySelectorAll('.jb-target option')].filter(x=>x.textContent.includes('(有効)')).length===4);
41
+ await area.locator('.jb-card').nth(1).click();
42
+ assert.equal(await area.locator('.jb-target').inputValue(),'');
43
+ assert.equal(await area.locator('.jb-apply-button').isDisabled(),true);
44
+ await area.locator('.jb-target').selectOption('4');
45
+ assert.equal(await area.locator('.jb-apply-button').isDisabled(),false);
46
+ console.log('PASS all-enabled slots require manual choice');
47
+ const file3=page.getByRole('combobox',{name:'LoRA 3 File',exact:true});
48
+ await file3.fill('---');
49
+ await page.getByRole('option').filter({hasText:'未選択・適用しない'}).first().click();
50
+ await page.waitForFunction(()=>[...document.querySelectorAll('label')].some(el=>el.innerText.includes('LoRA 3 Enable')&&el.querySelector('input')?.disabled));
51
+ assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),false);
52
+ console.log('PASS manual clear still disables the slot');
53
+ const file4=page.getByRole('combobox',{name:'LoRA 4 File',exact:true});
54
+ await file4.fill('---');
55
+ await page.getByRole('option').filter({hasText:'未選択・適用しない'}).first().click();
56
+ await page.getByRole('radio',{name:'非Turbo+Turbo LoRA',exact:true}).check();
57
+ await page.waitForFunction(()=>[...document.querySelectorAll('label')].some(el=>el.innerText.includes('LoRA 4 Enable')&&el.querySelector('input')?.checked&&!el.querySelector('input')?.disabled));
58
+ console.log('PASS preset restores and enables LoRA 4 after an empty selection');
59
+ await page.waitForFunction(()=>[...document.querySelectorAll('.jb-detail[open] .jb-preview img')].some(img=>img.complete&&img.naturalWidth>0),{},{timeout:45000});
60
+ if(process.env.JUJO_DESKTOP_SCREENSHOT){await area.screenshot({path:process.env.JUJO_DESKTOP_SCREENSHOT});}
61
+
62
+ // A separate mobile session starts with the same default state.
63
+ const mobile=await browser.newPage({viewport:{width:390,height:844},isMobile:true,hasTouch:true});
64
+ mobile.on('pageerror',e=>errors.push(e.message));
65
+ await mobile.goto('http://127.0.0.1:7868',{waitUntil:'domcontentloaded'});
66
+ await mobile.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click();
67
+ const ma=mobile.locator('.jujo-samples:visible');
68
+ await ma.locator('.jb-card').first().waitFor();
69
+ assert.equal(await ma.locator('.jb-card').count(),4);
70
+ await ma.locator('.jb-category').selectOption('目の形');
71
+ await ma.locator('.jb-next').click();
72
+ const before=await ma.locator('.jb-page').innerText();
73
+ const firstName=await ma.locator('.jb-card').first().getAttribute('data-name');
74
+ await ma.locator('.jb-card').first().click();
75
+ const dialog=ma.locator('.jb-detail');
76
+ assert.equal(await dialog.evaluate(el=>el.matches(':modal')),true);
77
+ assert.equal(await ma.locator('.jb-target').inputValue(),'3');
78
+ await ma.locator('.jb-preview').click();
79
+ assert.equal(await ma.locator('.jb-zoom').evaluate(el=>el.matches(':modal')),true);
80
+ await ma.locator('.jb-zoom-close').click();
81
+ await ma.locator('.jb-apply-button').click();
82
+ await ma.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました'}).waitFor();
83
+ await mobile.waitForFunction(()=>[...document.querySelectorAll('.jb-detail[open] .jb-preview img')].some(img=>img.complete&&img.naturalWidth>0),{},{timeout:45000});
84
+ if(process.env.JUJO_MOBILE_DETAIL_SCREENSHOT)await mobile.screenshot({path:process.env.JUJO_MOBILE_DETAIL_SCREENSHOT});
85
+ await ma.locator('.jb-close').click();
86
+ assert.equal(await dialog.isVisible(),false);
87
+ assert.equal(await ma.locator('.jb-page').innerText(),before);
88
+ assert.equal(await ma.locator('.jb-card').first().getAttribute('data-name'),firstName);
89
+ assert.equal(await ma.locator('.jb-category').inputValue(),'目の形');
90
+ assert.equal(await mobile.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);
91
+ if(process.env.JUJO_MOBILE_SCREENSHOT)await ma.screenshot({path:process.env.JUJO_MOBILE_SCREENSHOT});
92
+ console.log('PASS mobile 4 cards, modal detail/zoom, apply, close restores list position and category; no horizontal overflow');
93
+
94
+ await mobile.getByRole('tab',{name:'Illustrious',exact:true}).click();
95
+ await mobile.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click();
96
+ const ia=mobile.locator('.jujo-samples:visible');
97
+ await ia.locator('.jb-search').fill('tareme1_v20');
98
+ await ia.locator('.jb-card').first().click();
99
+ assert.match(await ia.locator('.jb-filename').innerText(),/hotarueye_xl_tareme1_v20/);
100
+ await ia.locator('.jb-target').selectOption('2');
101
+ await ia.locator('.jb-apply-button').click();
102
+ await ia.locator('.jb-message').filter({hasText:'LoRA 2に設定して有効化しました'}).waitFor();
103
+ await ia.locator('.jb-close').click();
104
+ await mobile.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
105
+ assert.equal(await mobile.getByRole('checkbox',{name:'LoRA 2 Enable',exact:true,includeHidden:true}).isChecked(),true);
106
+ assert.deepEqual(errors,[]);
107
+ console.log('PASS ILL slot assignment; no browser errors');
108
+ } finally {await browser.close();}
109
+ })().catch(e=>{console.error(e);process.exitCode=1;});
tests/lora_ui_browser.cjs ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Compatibility entry point; the responsive browser replaces the sample dropdown.
2
+ require('./jujo_browser_ui.cjs');
tests/lora_ui_demo.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render the real page builder without ComfyUI, weights, or GPU allocation."""
2
+
3
+ import ast
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import gradio as gr
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(ROOT))
12
+ from jujo_loras import JUJO_ENTRIES, NONE_LORA, jujo_label, lora_selected, sample_card, pony_selection_notice
13
+ from tag_completion import prompt_with_completion
14
+ from jujo_browser import build_jujo_browser
15
+ from browser_settings import bind_browser_settings
16
+
17
+ source = ast.parse((ROOT / "app.py").read_text(encoding="utf-8"))
18
+ for node in source.body:
19
+ if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name):
20
+ if node.targets[0].id in ("RESOLUTIONS", "SAMPLERS", "SCHEDULERS", "ANIMA_PRESETS", "ILLUSTRIOUS_PRESETS"):
21
+ globals()[node.targets[0].id] = ast.literal_eval(node.value)
22
+ builder = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "build_generation_page")
23
+ STATIC_INFO = "LoRA UI validation · No model loaded"
24
+ # Only isfile() is exercised; no embedding bytes are loaded in this fixture.
25
+ EMBEDDING_PATHS = {"test-embedding": __file__}
26
+
27
+
28
+ def generate_image_ui(*args):
29
+ return None, "Received LoRA slots: " + repr(args[11:23]) + " | seed " + str(3952492001372691585 if int(args[6]) < 0 else int(args[6]))
30
+
31
+
32
+ exec(compile(ast.Module(body=[builder], type_ignores=[]), "app.py", "exec"))
33
+
34
+ with gr.Blocks(analytics_enabled=False) as demo:
35
+ for family, label in (("anima", "Anima"), ("illustrious", "Illustrious")):
36
+ with gr.Tab(label):
37
+ preset_file = "Turbo-ANIMA-v2.safetensors" if family == "anima" else "dmd2_sdxl_4step_lora.safetensors"
38
+ build_generation_page(
39
+ family_label=label, model_choices=[family + "-checkpoint", family + "-checkpoint-2"],
40
+ lora_choices=["existing.safetensors", preset_file] + list(JUJO_ENTRIES[family]),
41
+ prompt_default="masterpiece, best quality,", negative_default="bad quality,",
42
+ steps_default=30, cfg_default=5, sampler_default="euler", scheduler_default="normal",
43
+ lora_defaults=[(True,"existing.safetensors",0.8), (False,"existing.safetensors",0.8),
44
+ (False,NONE_LORA,0.8), (False,preset_file,1.0)],
45
+ preset_specs=ANIMA_PRESETS if family == "anima" else ILLUSTRIOUS_PRESETS,
46
+ preset_default="turbo" if family == "anima" else "wai",
47
+ vae_choices=[("Checkpoint内蔵VAE", "embedded"), ("SDXL VAE", "sdxl")] if family == "illustrious" else None,
48
+ vae_default="embedded" if family == "illustrious" else None,
49
+ embedding_files=[("test-embedding", "", "SDXL")] if family == "illustrious" else None,
50
+ )
51
+
52
+ if __name__ == "__main__":
53
+ demo.launch(server_name="127.0.0.1", server_port=7868,
54
+ css=(ROOT / "lora_ui.css").read_text(encoding="utf-8"))
tests/pony_loras_ui.cjs ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {chromium} = require('playwright');
2
+ const assert = require('node:assert/strict');
3
+ (async()=>{
4
+ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})});
5
+ try {
6
+ for(const mobile of [false,true]) {
7
+ const page=await browser.newPage({viewport:mobile?{width:390,height:844}:{width:1440,height:1000},isMobile:mobile,hasTouch:mobile});
8
+ const errors=[]; page.on('pageerror', e=>errors.push(e.message));
9
+ await page.goto('http://127.0.0.1:7868');
10
+ await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
11
+ await page.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click();
12
+ const area=page.locator('.jujo-samples:visible');
13
+ assert.equal(await area.locator('.jb-pony-badge').count(),0);
14
+ await area.locator('.jb-search').fill('Pony用');
15
+ assert.match(await area.locator('.jb-count').innerText(),/^17件/);
16
+ assert.equal(await area.locator('.jb-pony-badge').count(),mobile?4:8);
17
+ await area.locator('.jb-search').fill('hotarueye_xl_comic02_v10');
18
+ await area.locator('.jb-card').click();
19
+ assert.match(await area.locator('.jb-code').innerText(),/Type 2 \/ Pony/);
20
+ assert.match(await area.locator('.jb-compatibility-warning').innerText(),/Pony用・ILL未検証/);
21
+ assert.match(await area.locator('.jb-sample-note').innerText(),/ILLでの再現は未確認/);
22
+ await area.locator('.jb-target').selectOption('3');
23
+ await area.locator('.jb-apply-button').click();
24
+ await area.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました。'}).waitFor();
25
+ if(mobile) await area.locator('.jb-close').click();
26
+ await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
27
+ const warning=page.locator('.pony-selection-warning:visible').first();
28
+ await warning.filter({hasText:'hotarueye_xl_comic02_v10.safetensors'}).waitFor();
29
+ assert.match(await warning.innerText(),/再現性は未検証/);
30
+ assert.deepEqual(errors,[]);
31
+ await page.close();
32
+ }
33
+ console.log('Pony labels, sample notices and slot selection warnings: desktop/mobile PASS');
34
+ } finally { await browser.close(); }
35
+ })().catch(e=>{console.error(e);process.exit(1);});
tests/tag_completion_browser.cjs ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // npm package: playwright. Run against tests/tag_completion_demo.py.
2
+ const { chromium } = require('playwright');
3
+ const assert = require('node:assert/strict');
4
+
5
+ (async () => {
6
+ const browser = await chromium.launch({
7
+ headless: true,
8
+ ...(process.env.TEST_CHROME ? { executablePath: process.env.TEST_CHROME } : {}),
9
+ });
10
+ try {
11
+ const page = await browser.newPage();
12
+ const errors = [], csvRequests = [];
13
+ page.on('pageerror', e => errors.push(e.message));
14
+ page.on('request', r => { if (r.url().includes('.csv')) csvRequests.push(r.url()); });
15
+ await page.goto('http://127.0.0.1:7867', { waitUntil: 'networkidle' });
16
+ const prompt = page.locator('#anima-positive-prompt textarea');
17
+ const wrapper = page.locator('.tag-completion.has_children').filter({ has: prompt });
18
+ await prompt.fill('blue h');
19
+ await wrapper.locator('.tc-option').first().waitFor();
20
+ assert.match(await wrapper.locator('.tc-name').first().innerText(), /^blue h/);
21
+ await prompt.press('Enter');
22
+ assert.match(await prompt.inputValue(), /^blue h[^,]+, $/);
23
+ console.log('PASS prefix completion and Enter');
24
+
25
+ await prompt.fill('masterpiece,\n(blue h:1.4), red eyes');
26
+ await prompt.evaluate(el => { const n=el.value.indexOf(':1.4'); el.setSelectionRange(n,n); el.dispatchEvent(new Event('click',{bubbles:true})); });
27
+ await wrapper.locator('.tc-option').first().waitFor();
28
+ await prompt.press('Tab');
29
+ assert.equal(await prompt.inputValue(), 'masterpiece,\n(blue hair:1.4), red eyes');
30
+ console.log('PASS middle tag, multiline, numeric weight, and Tab');
31
+
32
+ await prompt.fill('blue h');
33
+ await wrapper.locator('.tc-option').first().waitFor();
34
+ await prompt.press('ArrowDown');
35
+ const selected = await wrapper.locator('[aria-selected="true"] .tc-name').innerText();
36
+ await prompt.press('Enter');
37
+ assert.equal(await prompt.inputValue(), selected + ', ');
38
+ console.log('PASS arrow navigation');
39
+
40
+ await prompt.fill('blue h');
41
+ await wrapper.locator('.tc-option').first().waitFor();
42
+ await prompt.press('Escape');
43
+ assert.equal(await wrapper.locator('.tc-results').isVisible(), false);
44
+ assert.equal(await prompt.inputValue(), 'blue h');
45
+ await prompt.press('Shift+Enter');
46
+ assert.match(await prompt.inputValue(), /\n/);
47
+ console.log('PASS Escape and newline');
48
+
49
+ await prompt.fill('blue h');
50
+ await wrapper.locator('.tc-option').first().waitFor();
51
+ await prompt.evaluate(el => el.dispatchEvent(new CompositionEvent('compositionstart',{bubbles:true})));
52
+ assert.equal(await wrapper.locator('.tc-results').isVisible(), false);
53
+ await prompt.press('Enter');
54
+ assert.equal((await prompt.inputValue()).includes('blue hair'), false);
55
+ await prompt.evaluate(el => el.dispatchEvent(new CompositionEvent('compositionend',{bubbles:true})));
56
+ console.log('PASS IME composition guard');
57
+
58
+ await wrapper.locator('.tc-enabled').uncheck();
59
+ await prompt.fill('blue h');
60
+ await page.waitForTimeout(250);
61
+ assert.equal(await wrapper.locator('.tc-results').isVisible(), false);
62
+ await wrapper.locator('.tc-enabled').check();
63
+ await prompt.fill('best q');
64
+ await wrapper.locator('.tc-option').first().waitFor();
65
+ await prompt.press('Enter');
66
+ assert.equal(await prompt.inputValue(), 'best quality, ');
67
+ console.log('PASS disabled toggle and quality tags');
68
+
69
+ await prompt.fill('bluehair');
70
+ await wrapper.locator('.tc-option').first().waitFor();
71
+ await prompt.press('Enter');
72
+ assert.equal(await prompt.inputValue(), 'blue hair, ');
73
+ console.log('PASS alias lookup');
74
+ await prompt.fill('hatsune miku (ap');
75
+ await wrapper.locator('.tc-option').first().waitFor();
76
+ await prompt.press('Enter');
77
+ assert.equal(await prompt.inputValue(), 'hatsune miku \\(append\\), ');
78
+ console.log('PASS literal parentheses escaped');
79
+
80
+ await wrapper.locator('summary').click();
81
+ await wrapper.locator('[data-category="4"]').uncheck();
82
+ await prompt.fill('hatsune miku');
83
+ await wrapper.locator('.tc-option').first().waitFor();
84
+ assert.equal(await wrapper.locator('.tc-category-4').count(), 0);
85
+ await wrapper.locator('[data-category="4"]').check();
86
+ await wrapper.locator('summary').click();
87
+ console.log('PASS category filtering');
88
+ await prompt.fill('best q');
89
+ await wrapper.locator('.tc-option').first().waitFor();
90
+ await prompt.press('Enter');
91
+
92
+ const neg = page.locator('#anima-negative-prompt textarea');
93
+ await neg.fill('bad h');
94
+ await page.locator('.tag-completion.has_children').filter({has:neg}).locator('.tc-option').first().waitFor();
95
+ await neg.press('Enter');
96
+ assert.match(await neg.inputValue(), /^bad /);
97
+ await page.getByRole('tab', {name:'illustrious', exact:true}).click();
98
+ for (const polarity of ['positive','negative']) {
99
+ const input = page.locator(`#illustrious-${polarity}-prompt textarea`);
100
+ await page.waitForFunction(id => document.querySelector(`#${id} textarea`)?.hasAttribute('aria-autocomplete'), `illustrious-${polarity}-prompt`);
101
+ await input.click();
102
+ await input.pressSequentially('green e', {delay:40});
103
+ const scoped = page.locator('.tag-completion.has_children').filter({has:input});
104
+ await scoped.locator('.tc-option').first().waitFor();
105
+ await scoped.locator('.tc-option').first().click();
106
+ assert.equal(await input.inputValue(), 'green eyes, ');
107
+ }
108
+ console.log('PASS all four fields and pointer selection');
109
+
110
+ await page.getByRole('button',{name:'Check values',exact:true}).click();
111
+ await page.waitForFunction(() => document.body.innerText.includes('best quality, '));
112
+ const received = await page.locator('body').innerText();
113
+ assert.match(received,/best quality, /);
114
+ assert.match(received,/green eyes, /);
115
+ assert.equal(csvRequests.length, 2);
116
+ assert.deepEqual(errors, []);
117
+ console.log('PASS Textbox values received by Python; CSV fetched once; no browser errors');
118
+ if (process.env.TAG_COMPLETION_SCREENSHOT) {
119
+ const input = page.locator('#illustrious-positive-prompt textarea');
120
+ await input.fill('green e');
121
+ await page.locator('.tc-option').first().waitFor();
122
+ await page.screenshot({path: process.env.TAG_COMPLETION_SCREENSHOT, fullPage:true});
123
+ }
124
+ } finally { await browser.close(); }
125
+ })().catch(error => { console.error(error); process.exitCode = 1; });
tests/tag_completion_demo.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only browser fixture using the same prompt components as the Space.
2
+
3
+ Run from the repository root: python tests/tag_completion_demo.py
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
+
11
+ import gradio as gr
12
+
13
+ from tag_completion import prompt_with_completion
14
+
15
+
16
+ with gr.Blocks(analytics_enabled=False) as demo:
17
+ prompts = []
18
+ for family in ("anima", "illustrious"):
19
+ with gr.Tab(family):
20
+ for polarity in ("positive", "negative"):
21
+ prompts.append(prompt_with_completion(
22
+ label=polarity, value="", lines=3,
23
+ elem_id=f"{family}-{polarity}-prompt",
24
+ ))
25
+ output = gr.JSON(label="Received prompts")
26
+ gr.Button("Check values").click(lambda *values: list(values), prompts, output)
27
+
28
+ if __name__ == "__main__":
29
+ demo.launch(server_name="127.0.0.1", server_port=7867)
tests/test_admin_stats.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from unittest.mock import patch
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
11
+ import gradio as gr
12
+ from admin_stats import AdminStatistics, build_admin_statistics, make_report
13
+ from types import SimpleNamespace
14
+ from generation_stats import JST
15
+
16
+
17
+ class TestAdminStatistics(unittest.TestCase):
18
+ def setUp(self):
19
+ environment = patch.dict(os.environ, {"ADMIN_USERS": "yamiuchi"})
20
+ environment.start()
21
+ self.addCleanup(environment.stop)
22
+ self.data = {"version": 1, "timezone": "Asia/Tokyo", "counts": {
23
+ "2026-09-30": {"anima": {"old-model": 4}},
24
+ "2026-10-14": {"anima": {"a": 26}},
25
+ "2026-10-15": {"anima": {"a": 94}, "illustrious": {"b": 2}}}}
26
+ self.models = {"anima": ["a", "unused"], "illustrious": ["b"]}
27
+
28
+ def test_daily_monthly_zero_models_and_removed_models(self):
29
+ report = make_report(self.data, self.models, datetime(2026, 10, 15, tzinfo=JST))
30
+ self.assertIn(["Anima", "a", 26, 94], report["daily"])
31
+ self.assertIn(["Anima", "unused", 0, 0], report["daily"])
32
+ self.assertEqual(report["daily"][-1], ["合計", "", 26, 96])
33
+ self.assertIn(["2026-10", "Anima", "a", 120], report["monthly"])
34
+ self.assertIn(["2026-09", "Anima", "old-model", 4], report["monthly"])
35
+ self.assertIn(["2026-10", "合計", "", 122], report["monthly"])
36
+
37
+ def test_jst_month_boundary(self):
38
+ report = make_report(self.data, self.models, datetime(2026, 10, 31, 15, tzinfo=timezone.utc))
39
+ self.assertIn("2026-11-01", report["daily_headers"][-1])
40
+ self.assertEqual(report["monthly"][0][0], "2026-11")
41
+
42
+ def test_public_ui_without_login_and_safe_errors(self):
43
+ with gr.Blocks() as demo:
44
+ build_admin_statistics(demo, SimpleNamespace(path=None), self.models)
45
+ config = demo.get_config_file()
46
+ self.assertFalse(any(c["type"] == "loginbutton" for c in config["components"]))
47
+ callback = next(iter(demo.fns.values())).fn
48
+ with self.assertLogs("admin_stats", level="WARNING"):
49
+ result = callback()
50
+ self.assertTrue(result[0]["visible"])
51
+ self.assertNotIn("GENERATION_STATS_DIR", result[1])
52
+ self.assertEqual(result[2]["value"], [])
53
+ self.assertEqual(result[3]["value"], [])
54
+
55
+ def test_read_only_file_and_errors(self):
56
+ with tempfile.TemporaryDirectory() as directory:
57
+ path = Path(directory) / "daily-counts.json"
58
+ path.write_text(json.dumps(self.data), encoding="utf-8")
59
+ before = path.read_bytes(), path.stat().st_mtime_ns
60
+ report = AdminStatistics(path, self.models).fetch()
61
+ self.assertTrue(report["daily"])
62
+ self.assertEqual(before, (path.read_bytes(), path.stat().st_mtime_ns))
63
+ with self.assertRaises(ValueError):
64
+ AdminStatistics(None, self.models).fetch()
65
+ path.write_text("broken", encoding="utf-8")
66
+ with self.assertRaises(ValueError):
67
+ AdminStatistics(path, self.models).fetch()
68
+ self.assertEqual(path.read_text(), "broken")
69
+
70
+ def test_public_regardless_of_admin_variable(self):
71
+ with tempfile.TemporaryDirectory() as directory:
72
+ path = Path(directory) / "daily-counts.json"
73
+ path.write_text(json.dumps(self.data), encoding="utf-8")
74
+ for value in (None, "", " , , ", "yamiuchi", "other"):
75
+ with patch.dict(os.environ):
76
+ if value is None:
77
+ os.environ.pop("ADMIN_USERS", None)
78
+ else:
79
+ os.environ["ADMIN_USERS"] = value
80
+ self.assertTrue(AdminStatistics(path, self.models).fetch()["daily"])
81
+
82
+ def test_invalid_counts_rejected(self):
83
+ for invalid in (True, -1, "1", 10**100):
84
+ self.data["counts"]["2026-10-15"]["illustrious"]["b"] = invalid
85
+ with self.assertRaises(ValueError):
86
+ make_report(self.data, self.models, datetime(2026, 10, 15, tzinfo=JST))
87
+
88
+
89
+ if __name__ == "__main__":
90
+ unittest.main()
tests/test_branding.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Display branding must not migrate storage or alter the public API."""
2
+ import ast
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+
8
+
9
+ class TestBranding(unittest.TestCase):
10
+ def test_titles(self):
11
+ source = (ROOT / "app.py").read_text(encoding="utf-8")
12
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
13
+ self.assertIn('gr.Blocks(title="Anima Collection+ MCP")', source)
14
+ self.assertIn('"# Anima Collection+ MCP\\n"', source)
15
+ self.assertIn("title: Anima Collection+ MCP\n", readme)
16
+ tree = ast.parse(source)
17
+ credits = next(node.value for node in tree.body if isinstance(node, ast.Assign)
18
+ and isinstance(node.targets[0], ast.Name) and node.targets[0].id == "PROJECT_CREDITS")
19
+ self.assertIn("Anima Collection** をベース", ast.literal_eval(credits))
20
+
21
+ def test_export_and_legacy_storage(self):
22
+ source = (ROOT / "assets/settings/settings.js").read_text(encoding="utf-8")
23
+ self.assertIn('`anima-collection-plus-mcp-favorites-${props.family}.json`', source)
24
+ self.assertIn('["anima-collection-plus-mcp-favorites", "imggen-favorites"].includes(data.format)', source)
25
+ self.assertIn('`image-model-collection:settings:v1:${props.family}`', source)
26
+ self.assertIn('`image-model-collection:favorites:v1:${props.family}`', source)
tests/test_browser_settings.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import unittest
3
+ from pathlib import Path
4
+
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
6
+ import gradio as gr
7
+ from browser_settings import SettingsSchema
8
+
9
+
10
+ class TestBrowserSettings(unittest.TestCase):
11
+ def setUp(self):
12
+ with gr.Blocks(analytics_enabled=False):
13
+ controls = {
14
+ "model": gr.Dropdown(choices=["a", "b"], value="a"),
15
+ "prompt": gr.Textbox(value="default"),
16
+ "negative_prompt": gr.Textbox(value="negative"),
17
+ "steps": gr.Slider(6, 60, value=8, step=1),
18
+ "cfg": gr.Slider(1, 8, value=1, step=.1),
19
+ "width": gr.Slider(512, 1536, value=1024, step=16),
20
+ "seed": gr.Number(value=-1, precision=0),
21
+ "lora1_file": gr.Dropdown(choices=["---", "valid"], value="---"),
22
+ "lora1_enabled": gr.Checkbox(value=False, interactive=False),
23
+ "embedding1": gr.Checkbox(value=False, interactive=False),
24
+ }
25
+ self.schema = SettingsSchema(controls)
26
+
27
+ def test_allowlist_and_ranges(self):
28
+ raw = {"model": "https://evil/model", "steps": 1000, "cfg": float("nan"),
29
+ "width": 1027, "seed": 42, "token": "never stored", "embedding1": True}
30
+ clean = self.schema.sanitize(raw)
31
+ self.assertEqual(clean["model"], "a")
32
+ self.assertEqual(clean["steps"], 8)
33
+ self.assertEqual(clean["cfg"], 1)
34
+ self.assertEqual(clean["width"], 1024)
35
+ self.assertEqual(clean["seed"], 42)
36
+ self.assertNotIn("token", clean)
37
+ self.assertFalse(clean["embedding1"])
38
+
39
+ def test_exact_seed_strings(self):
40
+ seed = "3952492001372691585"
41
+ self.assertEqual(self.schema.sanitize({"seed": seed})["seed"], int(seed))
42
+ for value in ("no", "1e10", "18446744073709551616", "-2"):
43
+ self.assertEqual(self.schema.sanitize({"seed": value})["seed"], -1)
44
+
45
+ def test_prompts_opt_in_and_length(self):
46
+ raw = {"prompt": "private text", "negative_prompt": "private negative"}
47
+ self.assertEqual(self.schema.sanitize(raw)["prompt"], "default")
48
+ self.assertEqual(self.schema.sanitize(raw, True)["prompt"], "private text")
49
+ self.assertEqual(self.schema.sanitize({"prompt": "x" * 20001}, True)["prompt"], "default")
50
+
51
+ def test_restored_blank_slot_and_removed_file_disabled(self):
52
+ raw = {"lora1_file": "valid", "lora1_enabled": True}
53
+ self.assertTrue(self.schema.sanitize(raw)["lora1_enabled"])
54
+ for name in ("---", "removed", "../other.safetensors"):
55
+ raw["lora1_file"] = name
56
+ clean = self.schema.sanitize(raw)
57
+ self.assertEqual(clean["lora1_file"], "---")
58
+ self.assertFalse(clean["lora1_enabled"])
59
+
60
+ def test_invalid_shapes_and_huge_numbers_fall_back(self):
61
+ for raw in (None, [], "bad", {"seed": 10**1000, "steps": True, "model": {"url": "evil"}}):
62
+ clean = self.schema.sanitize(raw)
63
+ self.assertEqual(clean["seed"], -1)
64
+ self.assertEqual(clean["steps"], 8)
65
+ self.assertEqual(clean["model"], "a")
66
+
67
+
68
+ if __name__ == "__main__":
69
+ unittest.main()
tests/test_generation_stats.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import json
3
+ import sys
4
+ import tempfile
5
+ import threading
6
+ import unittest
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from types import SimpleNamespace
11
+ from unittest.mock import patch
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
14
+ from generation_stats import GenerationStats
15
+ from jujo_loras import lora_selected
16
+
17
+
18
+ class TestStatistics(unittest.TestCase):
19
+ def test_jst_date_parallel_counts_and_restore(self):
20
+ with tempfile.TemporaryDirectory() as directory:
21
+ counter = GenerationStats(directory)
22
+ moment = datetime(2026, 10, 14, 15, 1, tzinfo=timezone.utc)
23
+ with ThreadPoolExecutor(max_workers=8) as pool:
24
+ list(pool.map(lambda _: counter.record("anima", "model-a", completed_at=moment), range(500)))
25
+ counter.record("illustrious", "model-b", count=3, completed_at=moment)
26
+ counter.flush()
27
+ data = json.loads(counter.path.read_text(encoding="utf-8"))
28
+ self.assertEqual(data["counts"]["2026-10-15"], {"anima": {"model-a": 500}, "illustrious": {"model-b": 3}})
29
+ self.assertEqual(set(data), {"version", "timezone", "counts"})
30
+ restored = GenerationStats(directory)
31
+ # A successful generation before background restore is merged.
32
+ restored.record("anima", "model-a", completed_at=moment)
33
+ restored.flush()
34
+ self.assertEqual(restored.snapshot()["counts"]["2026-10-15"]["anima"]["model-a"], 501)
35
+ restored.flush()
36
+ self.assertEqual(restored.snapshot()["counts"]["2026-10-15"]["anima"]["model-a"], 501)
37
+
38
+ def test_save_failure_retries_and_unchanged_does_not_write(self):
39
+ with tempfile.TemporaryDirectory() as directory:
40
+ counter = GenerationStats(directory)
41
+ counter.record("anima", "a")
42
+ with patch("generation_stats.os.replace", side_effect=OSError("offline")):
43
+ with self.assertRaises(OSError):
44
+ counter.flush()
45
+ counter.record("anima", "a")
46
+ counter.flush()
47
+ with patch("generation_stats.os.replace", side_effect=AssertionError("unnecessary write")):
48
+ counter.flush()
49
+ self.assertEqual(sum(day["anima"]["a"] for day in counter.snapshot()["counts"].values()), 2)
50
+
51
+ def test_generation_record_does_not_wait_for_storage(self):
52
+ with tempfile.TemporaryDirectory() as directory:
53
+ counter = GenerationStats(directory)
54
+ counter.record("anima", "a")
55
+ entered, release, recorded = threading.Event(), threading.Event(), threading.Event()
56
+ real_write = Path.write_text
57
+
58
+ def slow_write(path, *args, **kwargs):
59
+ entered.set()
60
+ release.wait(3)
61
+ return real_write(path, *args, **kwargs)
62
+
63
+ with patch.object(Path, "write_text", slow_write):
64
+ writer = threading.Thread(target=counter.flush)
65
+ writer.start()
66
+ self.assertTrue(entered.wait(1))
67
+ recorder = threading.Thread(target=lambda: (counter.record("anima", "a"), recorded.set()))
68
+ recorder.start()
69
+ try:
70
+ self.assertTrue(recorded.wait(0.5), "generation blocked behind storage I/O")
71
+ finally:
72
+ release.set(); writer.join(); recorder.join()
73
+ counter.flush()
74
+ self.assertEqual(sum(day["anima"]["a"] for day in counter.snapshot()["counts"].values()), 2)
75
+
76
+ def test_corrupt_history_is_not_overwritten(self):
77
+ with tempfile.TemporaryDirectory() as directory:
78
+ counter = GenerationStats(directory)
79
+ counter.path.write_text("broken", encoding="utf-8")
80
+ counter.record("anima", "a")
81
+ with self.assertRaises(ValueError):
82
+ counter.flush()
83
+ self.assertEqual(counter.path.read_text(), "broken")
84
+
85
+ def test_background_save_and_memory_only_mode(self):
86
+ with tempfile.TemporaryDirectory() as directory:
87
+ counter = GenerationStats(directory, interval=0.02)
88
+ saved = threading.Event()
89
+ real_replace = __import__("os").replace
90
+ def replace(*args):
91
+ real_replace(*args); saved.set()
92
+ with patch("generation_stats.os.replace", replace):
93
+ counter.start()
94
+ counter.record("illustrious", "a")
95
+ try:
96
+ self.assertTrue(saved.wait(2))
97
+ finally:
98
+ counter.stop()
99
+ memory = GenerationStats()
100
+ memory.record("anima", "a")
101
+ memory.flush()
102
+ self.assertIsNone(memory.path)
103
+
104
+ def test_common_ui_mcp_hook_counts_only_success_after_gpu(self):
105
+ source = ast.parse((Path(__file__).resolve().parents[1] / "app.py").read_text(encoding="utf-8"))
106
+ function = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "serialized_generate")
107
+ actions = []
108
+ def gpu(*args, **kwargs):
109
+ actions.append("gpu")
110
+ return object(), "info", "positive", "negative"
111
+ env = dict(
112
+ generation_lock=threading.Lock(), MODEL_FAMILY={"a": "anima"},
113
+ lora_selected=lora_selected, _generate_image_core=gpu,
114
+ generation_stats=SimpleNamespace(record=lambda *a: actions.append(("count", a))),
115
+ )
116
+ exec(compile(ast.Module(body=[function], type_ignores=[]), "app.py", "exec"), env)
117
+ args = ["a"] + [None] * 9 + [False, "---", .8] * 4 + [None] * 3
118
+ env["serialized_generate"](*args)
119
+ self.assertEqual(actions, ["gpu", ("count", ("anima", "a"))])
120
+ actions.clear()
121
+ env["_generate_image_core"] = lambda *a, **kw: (_ for _ in ()).throw(RuntimeError("failed"))
122
+ with self.assertRaises(RuntimeError):
123
+ env["serialized_generate"](*args)
124
+ self.assertEqual(actions, [])
125
+
126
+
127
+ if __name__ == "__main__":
128
+ unittest.main()
tests/test_jujo_browser.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import re
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
7
+ import gradio as gr
8
+ from jujo_browser import assignment, entries_for_browser
9
+ from scripts.update_jujo_catalog import image_path, pupil_sample_viewport
10
+
11
+
12
+ class TestAssignment(unittest.TestCase):
13
+ def setUp(self):
14
+ self.values = [True, "a", .8, False, "b", .7, False, "---", .8, False, "c", 1.0]
15
+ self.name = entries_for_browser("anima")[0]["name"]
16
+
17
+ def test_only_selected_slot_changes(self):
18
+ payload = dict(selected=self.name, slot=3, strength=.9)
19
+ offset, updated = assignment("anima", payload, self.values)
20
+ self.assertEqual(offset, 6)
21
+ self.assertEqual(updated[6:9], [True, self.name, .9])
22
+ self.assertEqual(updated[:6], self.values[:6])
23
+ self.assertEqual(updated[9:], self.values[9:])
24
+ self.assertFalse(self.values[6])
25
+
26
+ def test_invalid_or_cross_family_action_rejected(self):
27
+ for patch in (dict(slot=0), dict(slot=5), dict(slot=True), dict(slot="1"),
28
+ dict(strength=float("nan")), dict(strength=3), dict(strength=-1),
29
+ dict(selected=entries_for_browser("illustrious")[0]["name"])):
30
+ payload = dict(selected=self.name, slot=1, strength=.8)
31
+ payload.update(patch)
32
+ with self.assertRaises(gr.Error):
33
+ assignment("anima", payload, self.values)
34
+
35
+ def test_every_entry_has_browser_metadata(self):
36
+ categories = {"目の形", "眉", "口", "表情・人物", "効果"}
37
+ for family in ("anima", "illustrious"):
38
+ for entry in entries_for_browser(family):
39
+ self.assertIn(entry["category"], categories)
40
+ self.assertTrue(entry["image"].startswith("https://huggingface.co/JujoHotaru/"))
41
+ self.assertTrue(0 <= entry["strength"] <= 2)
42
+
43
+ def test_eye_collection_uses_exact_code_ill_samples(self):
44
+ entries = [e for e in entries_for_browser("illustrious") if e["name"].startswith("eyecolle_xl_code")]
45
+ self.assertEqual(len(entries), 81)
46
+ self.assertEqual(len({e["image"] for e in entries}), 81)
47
+ for entry in entries:
48
+ code = re.fullmatch(r"eyecolle_xl_code(\d{3})\.safetensors", entry["name"])[1]
49
+ self.assertRegex(entry["image"], rf"/(tareme|tsurime)_ill{code}\.webp$")
50
+ self.assertFalse(entry["group_sample"])
51
+ self.assertEqual(entry["code"], "code" + code)
52
+ self.assertIn(entry["code"], entry["sample_note"])
53
+ self.assertIn("Illustrious", entry["sample_note"])
54
+ shape = "tareme" if "/tareme_" in entry["image"] else "tsurime"
55
+ self.assertIn(shape, entry["hint"])
56
+ self.assertEqual(entry["strength"], 1.5)
57
+
58
+ def test_catalog_generator_prefers_ill_not_pony_or_other_code(self):
59
+ doc = "sdxl/eyecollexl/README.md"
60
+ base = "sdxl/eyecollexl/"
61
+ path = base + "eyecolle_xl_code127.safetensors"
62
+ markdown = "![](thumb.webp)\n![](tareme_pny127.webp)![](tareme_ill1270.webp)![](tareme_ill127.webp)"
63
+ available = {base + name for name in ("thumb.webp", "tareme_pny127.webp", "tareme_ill1270.webp", "tareme_ill127.webp")}
64
+ self.assertEqual(image_path(markdown, "JujoHotaru/lora", "pinned", doc, path, available),
65
+ (base + "tareme_ill127.webp", False))
66
+ available.remove(base + "tareme_ill127.webp")
67
+ self.assertEqual(image_path(markdown, "JujoHotaru/lora", "pinned", doc, path, available),
68
+ (base + "thumb.webp", True))
69
+
70
+ def test_eye_type_metadata_and_individual_samples(self):
71
+ entries = {e["name"]: e for e in entries_for_browser("illustrious")}
72
+ for code, version in (("01", "20"), ("04", "10")):
73
+ entry = entries[f"hotarueye_xl_comic{code}_v{version}_ill.safetensors"]
74
+ self.assertTrue(entry["image"].endswith(f"/sdxl/hotarueye_xl_comic{code}_v{version}.webp"))
75
+ self.assertIn(f"Type {int(code)}", entry["sample_note"])
76
+ self.assertEqual(entry["category"], "目の形")
77
+ self.assertFalse(entry["group_sample"])
78
+ for number in (1, 2):
79
+ entry = entries[f"hotarueye_xl_hosome{number}_v30.safetensors"]
80
+ self.assertTrue(entry["image"].endswith(f"/sdxl/hosome/type{number}_v30.webp"))
81
+ self.assertIn(f"Type {number}", entry["sample_note"])
82
+ self.assertIn("Illustrious", entry["sample_note"])
83
+ self.assertFalse(entry["group_sample"])
84
+ for number, xy in enumerate(((768,0),(1168,0),(1568,0),(768,400),(1168,400),(1568,400)), 1):
85
+ entry = entries[f"hotarupupil_xl_type{number}_v10.safetensors"]
86
+ view = entry["sample_viewport"]
87
+ self.assertTrue(entry["image"].endswith("/sdxl/pupil/v10_sample1.webp"))
88
+ self.assertEqual((view["x"], view["y"]), xy)
89
+ self.assertEqual((view["width"], view["height"]), (400,400))
90
+ self.assertLessEqual(view["x"] + view["width"], view["source_width"])
91
+ self.assertLessEqual(view["y"] + view["height"], view["source_height"])
92
+ self.assertIn(f"Type {number}", entry["sample_note"])
93
+ self.assertTrue(entry["group_sample"]) # source is still a comparison sheet
94
+
95
+ def test_type_sample_generator_handles_author_filenames(self):
96
+ def pick(doc, name, images):
97
+ folder = str(Path(doc).parent).replace("\\", "/")
98
+ available = {f"{folder}/{image}" for image in images}
99
+ text = "".join(f"![]({image})" for image in images)
100
+ return image_path(text, "JujoHotaru/lora", "pinned", doc, "sdxl/" + name, available)
101
+ self.assertEqual(pick("sdxl/README.md", "hotarueye_xl_comic01_v20_ill.safetensors",
102
+ ["header.webp","hotarueye_xl_comic01_v10.webp","hotarueye_xl_comic01_v20.webp"]),
103
+ ("sdxl/hotarueye_xl_comic01_v20.webp", False))
104
+ self.assertEqual(pick("sdxl/hosome/README.md", "hotarueye_xl_hosome2_v30.safetensors",
105
+ ["thumb.webp","type1_v30.webp","type2_v30.webp"]),
106
+ ("sdxl/hosome/type2_v30.webp", False))
107
+ self.assertEqual(pick("sdxl/pupil/README.md", "hotarupupil_xl_type6_v10.safetensors",
108
+ ["thumb.webp","v10_sample1.webp","v10_sample2.webp"]),
109
+ ("sdxl/pupil/v10_sample1.webp", True))
110
+ self.assertIsNone(pupil_sample_viewport("hotarupupil_xl_type7_v10.safetensors", "sdxl/pupil/v10_sample1.webp"))
111
+ self.assertIsNone(pupil_sample_viewport("hotarupupil_xl_type1_v10.safetensors", "sdxl/pupil/v10_sample2.webp"))
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
tests/test_jujo_loras.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only regression checks for LoRA families and pre-GPU preparation."""
2
+
3
+ import ast
4
+ import os
5
+ import sys
6
+ import threading
7
+ import unittest
8
+ from pathlib import Path
9
+ from types import SimpleNamespace
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
+ from jujo_loras import CATALOGS, JUJO_ENTRIES, NONE_LORA, lora_selected, sample_card, jujo_label, pony_selection_notice
13
+
14
+
15
+ class TestAuthorLoras(unittest.TestCase):
16
+ def test_no_selection_is_never_active(self):
17
+ for enabled in (False, True):
18
+ for name in (NONE_LORA, "", None):
19
+ self.assertFalse(lora_selected(enabled, name))
20
+ self.assertTrue(lora_selected(True, "a.safetensors"))
21
+
22
+ def test_family_catalogs_and_samples(self):
23
+ anima, ill = JUJO_ENTRIES["anima"], JUJO_ENTRIES["illustrious"]
24
+ self.assertTrue(anima and ill)
25
+ self.assertFalse(set(anima) & set(ill))
26
+ self.assertIn("hotarueye_xl_tareme1_v20.safetensors", ill)
27
+ self.assertNotIn("hotarueye_xl_tareme1_v10.safetensors", ill)
28
+ self.assertTrue(ill["hotarueye_xl_comic02_v10.safetensors"]["pony_only"])
29
+ self.assertEqual(len(ill), 188)
30
+ self.assertEqual(sum(bool(e.get("pony_only")) for e in ill.values()), 17)
31
+ self.assertEqual([bool(e.get("pony_only")) for e in ill.values()], [False]*171+[True]*17)
32
+ for family, entries in JUJO_ENTRIES.items():
33
+ for name, entry in entries.items():
34
+ self.assertIsNotNone(entry["image"])
35
+ if family == "illustrious":
36
+ self.assertTrue(entry["path"].startswith("sdxl/"))
37
+ if "_pony" in name:
38
+ self.assertTrue(entry.get("pony_only"))
39
+ self.assertIn(CATALOGS[family]["revision"], sample_card(family, name))
40
+
41
+ def test_pony_notices(self):
42
+ for name, entry in JUJO_ENTRIES["illustrious"].items():
43
+ if entry.get("pony_only"):
44
+ self.assertIn("Pony用・ILL未検証", jujo_label("illustrious", name))
45
+ self.assertIn(name, pony_selection_notice("illustrious", [name]))
46
+ self.assertIn("未検証", sample_card("illustrious", name))
47
+ else:
48
+ self.assertEqual(pony_selection_notice("illustrious", [name]), "")
49
+ self.assertEqual(pony_selection_notice("anima", ["unknown", NONE_LORA]), "")
50
+
51
+ def test_download_before_gpu_and_reject_other_family(self):
52
+ source = ast.parse((Path(__file__).resolve().parents[1] / "app.py").read_text(encoding="utf-8"))
53
+ function = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "serialized_generate")
54
+ actions = []
55
+ env = dict(
56
+ generation_lock=threading.Lock(), MODEL_FAMILY={"anima": "anima", "ill": "illustrious"},
57
+ FAMILY_LORA_NAMES={key: set(value) for key, value in JUJO_ENTRIES.items()},
58
+ JUJO_ENTRIES=JUJO_ENTRIES, JUJO_CATALOGS=CATALOGS,
59
+ lora_selected=lora_selected, gr=SimpleNamespace(Error=ValueError),
60
+ ensure_model_file=lambda *a, **kw: actions.append(("download", a, kw)),
61
+ MODELS_ROOT="models", os=os,
62
+ _generate_image_core=lambda *a, **kw: actions.append(("gpu",)),
63
+ )
64
+ exec(compile(ast.Module(body=[function], type_ignores=[]), "app.py", "exec"), env)
65
+ args = ["anima", "", "", 1024, 1024, 1, 8, 1, "euler", "normal"]
66
+ args += [True, NONE_LORA, 0.8] * 4
67
+ args += [None, None, lambda *a, **kw: None]
68
+ env["serialized_generate"](*args)
69
+ self.assertEqual(actions, [("gpu",)])
70
+ actions.clear()
71
+ name = next(iter(JUJO_ENTRIES["anima"]))
72
+ args[11] = name
73
+ env["serialized_generate"](*args)
74
+ self.assertEqual([x[0] for x in actions], ["download", "gpu"])
75
+ self.assertEqual(actions[0][2]["revision"], CATALOGS["anima"]["revision"])
76
+ self.assertEqual(actions[0][1][1], "")
77
+ actions.clear()
78
+ args[11] = next(iter(JUJO_ENTRIES["illustrious"]))
79
+ with self.assertRaises(ValueError):
80
+ env["serialized_generate"](*args)
81
+ self.assertEqual(actions, [])
82
+
83
+
84
+ if __name__ == "__main__":
85
+ unittest.main()