damoncao commited on
Commit
6b19e2a
·
verified ·
1 Parent(s): 6152b5d

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* 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
 
 
 
 
 
 
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
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
+ images/01-regen.png filter=lfs diff=lfs merge=lfs -text
38
+ images/01.png filter=lfs diff=lfs merge=lfs -text
39
+ images/02-regen.png filter=lfs diff=lfs merge=lfs -text
40
+ images/02.png filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Alibaba Cloud
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md CHANGED
@@ -1,3 +1,317 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: transformers
4
+ pipeline_tag: image-text-to-text
5
+ tags:
6
+ - anime
7
+ - image-to-prompt
8
+ - image-tagging
9
+ - danbooru
10
+ - qwen3.5
11
+ - vision-language
12
  ---
13
+
14
+ # Anime_Image2Prompt
15
+
16
+ [English](#english) | [中文说明](#中文说明)
17
+
18
+ ## English
19
+
20
+ ### Model overview
21
+
22
+ **Anime_Image2Prompt** is a Qwen3.5-2B-based vision-language model that converts a single anime, illustration, or anime-style image into Danbooru-style prompt tags. It separates general visual attributes, character identities, and copyrights/franchises, making it useful for image organization, dataset annotation, and prompt extraction in generative-image workflows.
23
+
24
+ The repository contains all weights required for inference; no base model or separate adapter is needed. The model is stored in BF16, occupies approximately 4.12 GiB, and has 2,213,241,664 parameters. The included program loads local files from this directory by default.
25
+
26
+ ### Original-to-regeneration comparison
27
+
28
+ The following two pairs demonstrate the complete workflow. `01.png` and `02.png` are the original images. Each original was processed by **Anime_Image2Prompt**, and the extracted prompt was passed without manual rewriting directly to [WAI-illustrious-SDXL v15.0](https://civitai.red/models/827184/wai-illustrious-sdxl?modelVersionId=2167369). The resulting images are `01-regen.png` and `02-regen.png`, respectively.
29
+
30
+ <table>
31
+ <tr>
32
+ <th width="50%">01 · Original</th>
33
+ <th width="50%">01 · Prompt regeneration</th>
34
+ </tr>
35
+ <tr>
36
+ <td><img src="images/01.png" alt="Example 01 original" width="100%"></td>
37
+ <td><img src="images/01-regen.png" alt="Example 01 prompt regeneration" width="100%"></td>
38
+ </tr>
39
+ <tr>
40
+ <th width="50%">02 · Original</th>
41
+ <th width="50%">02 · Prompt regeneration</th>
42
+ </tr>
43
+ <tr>
44
+ <td><img src="images/02.png" alt="Example 02 original" width="100%"></td>
45
+ <td><img src="images/02-regen.png" alt="Example 02 prompt regeneration" width="100%"></td>
46
+ </tr>
47
+ </table>
48
+
49
+ These examples illustrate how the model captures major semantics such as character identity, clothing, scene, and composition. Prompt regeneration is not image-to-image or pixel-level reconstruction; the final image also depends on the generation model, random seed, and sampling settings.
50
+
51
+ ### Output format
52
+
53
+ The model's native output is a fixed JSON object:
54
+
55
+ ```json
56
+ {
57
+ "general_tags": ["1girl,long hair,blue eyes"],
58
+ "character_tags": ["example character"],
59
+ "copyright_tags": ["example copyright"]
60
+ }
61
+ ```
62
+
63
+ - `general_tags`: counts, appearance, clothing, actions, composition, and scene attributes.
64
+ - `character_tags`: character names recognized by the model.
65
+ - `copyright_tags`: recognized works or franchises.
66
+ - Each non-empty category contains one comma-separated string. An unknown category is returned as an empty array, `[]`.
67
+
68
+ The included program can also combine all categories into a flat prompt:
69
+
70
+ ```text
71
+ 1girl, long hair, blue eyes, example character, example copyright
72
+ ```
73
+
74
+ ### Requirements
75
+
76
+ - Python 3.10 or newer; Python 3.11 is recommended.
77
+ - An NVIDIA GPU with at least 8 GB of VRAM is recommended. More VRAM provides additional headroom.
78
+ - CPU inference is supported but slow; at least 16 GB of system RAM is recommended.
79
+ - The weight file is larger than 4 GB. Use a large-file-capable filesystem such as NTFS, exFAT, APFS, or ext4 instead of FAT32.
80
+
81
+ ### Installation
82
+
83
+ Create a virtual environment inside the model directory:
84
+
85
+ ```bash
86
+ python -m venv .venv
87
+ ```
88
+
89
+ Windows PowerShell:
90
+
91
+ ```powershell
92
+ .\.venv\Scripts\Activate.ps1
93
+ python -m pip install --upgrade pip
94
+ python -m pip install -r requirements.txt
95
+ ```
96
+
97
+ Linux / macOS:
98
+
99
+ ```bash
100
+ source .venv/bin/activate
101
+ python -m pip install --upgrade pip
102
+ python -m pip install -r requirements.txt
103
+ ```
104
+
105
+ If you need a specific CUDA build, install the PyTorch build matching your GPU driver from the [official PyTorch selector](https://pytorch.org/get-started/locally/) before installing the remaining requirements.
106
+
107
+ ### Quick start
108
+
109
+ The script resides beside the model files, so no model path is required:
110
+
111
+ ```bash
112
+ python inference.py /path/to/image.png
113
+ ```
114
+
115
+ The default output is structured JSON. Save it to a file with:
116
+
117
+ ```bash
118
+ python inference.py /path/to/image.png --output result.json
119
+ ```
120
+
121
+ Print a flat, copy-ready prompt instead:
122
+
123
+ ```bash
124
+ python inference.py /path/to/image.png --format prompt
125
+ ```
126
+
127
+ Useful options:
128
+
129
+ ```bash
130
+ # Force CPU inference
131
+ python inference.py image.png --device cpu
132
+
133
+ # Reduce image tokens and peak VRAM usage
134
+ python inference.py image.png --low-vram
135
+
136
+ # Show every option
137
+ python inference.py --help
138
+ ```
139
+
140
+ The program applies EXIF orientation and RGB conversion automatically and accepts common image formats supported by Pillow. Model loading uses `local_files_only=True`, so it does not download additional weights.
141
+
142
+ ### Python API
143
+
144
+ ```python
145
+ from inference import AnimeImage2Prompt
146
+
147
+ tagger = AnimeImage2Prompt()
148
+ result = tagger.predict("image.png")
149
+
150
+ print(result)
151
+ print(tagger.to_prompt(result))
152
+ ```
153
+
154
+ Reuse the same `AnimeImage2Prompt` instance for multiple images to avoid reloading the model.
155
+
156
+ ### Limitations
157
+
158
+ - The model is intended primarily for anime and illustration images. Performance may degrade on photographs or images far outside its training distribution.
159
+ - Tags are not guaranteed to be exhaustive or correct. Character and franchise recognition can be especially uncertain.
160
+ - The model may return tags describing sensitive content. Apply content filtering appropriate to your use case and follow applicable laws and platform policies.
161
+ - Do not use the model as the sole basis for safety moderation, identity decisions, or other high-impact decisions.
162
+
163
+ ### License
164
+
165
+ This repository is released under the Apache License 2.0. See [`LICENSE`](LICENSE) for details.
166
+
167
+ ---
168
+
169
+ ## 中文说明
170
+
171
+ ### 模型简介
172
+
173
+ **Anime_Image2Prompt** 是一个基于 Qwen3.5-2B 的动漫图像理解模型,可将单张动漫、插画或二次元风格图片转换为 Danbooru 风格的提示词。模型会分别识别画面通用特征、角色名称和作品来源,适合用于图片归档、数据集标注和生成式图像工作流中的提示词提取。
174
+
175
+ 模型权重已完整包含在本仓库中,无需额外下载基础模型或适配器。权重以 BF16 保存,约 4.12 GiB,共 2,213,241,664 个参数。推理程序默认只读取当前目录中的本地文件。
176
+
177
+ ### 原图与提示词重绘对比
178
+
179
+ 下面展示两组端到端效果对比。`01.png` 和 `02.png` 是原图;处理流程为:将原图输入 **Anime_Image2Prompt**,把模型提取出的提示词不经人工改写,直接交给 [WAI-illustrious-SDXL v15.0](https://civitai.red/models/827184/wai-illustrious-sdxl?modelVersionId=2167369) 绘制,对应结果分别为 `01-regen.png` 和 `02-regen.png`。
180
+
181
+ <table>
182
+ <tr>
183
+ <th width="50%">01 · 原图</th>
184
+ <th width="50%">01 · 提示词重绘</th>
185
+ </tr>
186
+ <tr>
187
+ <td><img src="images/01.png" alt="示例 01 原图" width="100%"></td>
188
+ <td><img src="images/01-regen.png" alt="示例 01 提示词重绘结果" width="100%"></td>
189
+ </tr>
190
+ <tr>
191
+ <th width="50%">02 · 原图</th>
192
+ <th width="50%">02 · 提示词重绘</th>
193
+ </tr>
194
+ <tr>
195
+ <td><img src="images/02.png" alt="示例 02 原图" width="100%"></td>
196
+ <td><img src="images/02-regen.png" alt="示例 02 提示词重绘结果" width="100%"></td>
197
+ </tr>
198
+ </table>
199
+
200
+ 这些示例用于观察模型对角色、服饰、场景和构图等主要语义的提取能力。提示词重绘并非图生图或像素级复刻,具体画面仍会受到绘图模型、随机种子和采样参数影响。
201
+
202
+ ### 输出格式
203
+
204
+ 模型原生输出固定为 JSON:
205
+
206
+ ```json
207
+ {
208
+ "general_tags": ["1girl,long hair,blue eyes"],
209
+ "character_tags": ["example character"],
210
+ "copyright_tags": ["example copyright"]
211
+ }
212
+ ```
213
+
214
+ - `general_tags`:人物数量、外观、服饰、动作、构图和场景等通用标签。
215
+ - `character_tags`:模型能够识别的角色名称。
216
+ - `copyright_tags`:模型能够识别的作品或系列名称。
217
+ - 每个非空类别包含一个以英文逗号连接的字符串;无法识别时返回空数组 `[]`。
218
+
219
+ 随附程序也可以把三个类别合并为常用的扁平提示词:
220
+
221
+ ```text
222
+ 1girl, long hair, blue eyes, example character, example copyright
223
+ ```
224
+
225
+ ### 环境要求
226
+
227
+ - Python 3.10 或更高版本,推荐 Python 3.11。
228
+ - 推荐使用具有 8 GB 或更多显存的 NVIDIA GPU;显存越大,运行越稳定。
229
+ - CPU 可以运行,但速度较慢,建议至少准备 16 GB 系统内存。
230
+ - 模型文件大于 4 GB,请使用 NTFS、exFAT、APFS、ext4 等支持大文件的文件系统,不要使用 FAT32。
231
+
232
+ ### 安装
233
+
234
+ 在模型目录中创建虚拟环境并安装依赖:
235
+
236
+ ```bash
237
+ python -m venv .venv
238
+ ```
239
+
240
+ Windows PowerShell:
241
+
242
+ ```powershell
243
+ .\.venv\Scripts\Activate.ps1
244
+ python -m pip install --upgrade pip
245
+ python -m pip install -r requirements.txt
246
+ ```
247
+
248
+ Linux / macOS:
249
+
250
+ ```bash
251
+ source .venv/bin/activate
252
+ python -m pip install --upgrade pip
253
+ python -m pip install -r requirements.txt
254
+ ```
255
+
256
+ 如果需要特定 CUDA 版本,请先按照 [PyTorch 官方安装说明](https://pytorch.org/get-started/locally/) 安装与显卡驱动匹配的 PyTorch,再执行依赖安装命令。
257
+
258
+ ### 快速使用
259
+
260
+ 推理脚本与模型放在同一目录,因此无需填写模型路径:
261
+
262
+ ```bash
263
+ python inference.py /path/to/image.png
264
+ ```
265
+
266
+ 默认在终端输出结构化 JSON。保存结果:
267
+
268
+ ```bash
269
+ python inference.py /path/to/image.png --output result.json
270
+ ```
271
+
272
+ 直接输出可复制的扁平提示词:
273
+
274
+ ```bash
275
+ python inference.py /path/to/image.png --format prompt
276
+ ```
277
+
278
+ 常用选项:
279
+
280
+ ```bash
281
+ # 强制使用 CPU
282
+ python inference.py image.png --device cpu
283
+
284
+ # 降低图像 token 数以减少峰值显存占用
285
+ python inference.py image.png --low-vram
286
+
287
+ # 查看全部参数
288
+ python inference.py --help
289
+ ```
290
+
291
+ 程序会自动处理图片的 EXIF 方向并转换为 RGB,支持 Pillow 可读取的常见图片格式。加载模型时使用 `local_files_only=True`,不会联网下载其他权重。
292
+
293
+ ### Python 调用
294
+
295
+ ```python
296
+ from inference import AnimeImage2Prompt
297
+
298
+ tagger = AnimeImage2Prompt()
299
+ result = tagger.predict("image.png")
300
+
301
+ print(result)
302
+ print(tagger.to_prompt(result))
303
+ ```
304
+
305
+ 同一个 `AnimeImage2Prompt` 实例可连续处理多张图片,模型只会加载一次。
306
+
307
+ ### 使用限制
308
+
309
+ - 本模型主要面向动漫和插画图片,真实照片或与训练分布差异较大的图片可能表现较差。
310
+ - 标签并不保证完整或完全准确,角色与作品来源尤其可能发生误识别。
311
+ - 模型可能输出敏感内容标签。使用者应根据应用场景进行内容过滤,并遵守当地法律及平台规则。
312
+ - 本模型不应作为安全审核、身份判断或其他高风险决策的唯一依据。
313
+
314
+ ### 许可证
315
+
316
+ 本仓库依据 Apache License 2.0 发布,详情见 [`LICENSE`](LICENSE)。
317
+
SHA256SUMS ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930 .gitattributes
2
+ 273d8e0e683b885071fb17e08d71e5f2a5ddfb5309756181681de4f5a1822d80 chat_template.jinja
3
+ 137bf1846d31c6a192563742fe779d4cdc70ace03f89c8ab75853bb1bd980011 config.json
4
+ 774d3ef576350368f17c9d5840e58b45b6f96e1027d614c7a086e38a07bb1897 generation_config.json
5
+ 0ec7a0f616ab7807fffdf79f8aad21099d745721902530019636ba4738387bb2 inference.py
6
+ 52496bd91e81ff8f5dfdb2edc8b9985563af531a7db3c5250473033a36f1c2da inference_config.json
7
+ 07226f7a7f661ef0cb3ce5fbef78f5cc8ee6952a430db07a4661c1c0a2bd57e7 images/01-regen.png
8
+ 2a6f2ddca81021930896da8c21d776419e2d2a0b5f60557fdb1991a91a3821e7 images/01.png
9
+ abbeaaaf80623948a0980bc6f9ad062fef49babb2c803535735efc3d7425aa5a images/02-regen.png
10
+ 98819176c7d08ba1981ee039af36e8769ae46073a7f7e15186dd2b506773a0d4 images/02.png
11
+ bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a LICENSE
12
+ 8e7b265c171b46ee7e3dd4809b3b1cfc933c40cbdbe5c976f499e654daa0be3b model.safetensors
13
+ 04920fe4d2d3491ed36e324231199cf5c1576fa41a34e0eff432bbe1e86ce5d8 processor_config.json
14
+ 598544c992ff9d934b44c7b01addf3dc1887e5b52f1cdf3882670970f538cc12 README.md
15
+ 1b8b6fbefca746644cb6a0e0fc5d541d54689f21b1e26a4ce0d11155353a31c8 requirements.txt
16
+ 06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523 tokenizer.json
17
+ 2a364f7d378ea19bdeedb7ab23b3f57d661a8aca282532a6d8e9e38d2252d740 tokenizer_config.json
chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- elif content is iterable and content is not mapping %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if is_system_content %}
10
+ {{- raise_exception('System message cannot contain images.') }}
11
+ {%- endif %}
12
+ {%- if do_vision_count %}
13
+ {%- set image_count.value = image_count.value + 1 %}
14
+ {%- endif %}
15
+ {%- if add_vision_id %}
16
+ {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
+ {%- endif %}
18
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
+ {%- elif 'video' in item or item.type == 'video' %}
20
+ {%- if is_system_content %}
21
+ {{- raise_exception('System message cannot contain videos.') }}
22
+ {%- endif %}
23
+ {%- if do_vision_count %}
24
+ {%- set video_count.value = video_count.value + 1 %}
25
+ {%- endif %}
26
+ {%- if add_vision_id %}
27
+ {{- 'Video ' ~ video_count.value ~ ': ' }}
28
+ {%- endif %}
29
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
+ {%- elif 'text' in item %}
31
+ {{- item.text }}
32
+ {%- else %}
33
+ {{- raise_exception('Unexpected item type in content.') }}
34
+ {%- endif %}
35
+ {%- endfor %}
36
+ {%- elif content is none or content is undefined %}
37
+ {{- '' }}
38
+ {%- else %}
39
+ {{- raise_exception('Unexpected content type.') }}
40
+ {%- endif %}
41
+ {%- endmacro %}
42
+ {%- if not messages %}
43
+ {{- raise_exception('No messages provided.') }}
44
+ {%- endif %}
45
+ {%- if tools and tools is iterable and tools is not mapping %}
46
+ {{- '<|im_start|>system\n' }}
47
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
+ {%- for tool in tools %}
49
+ {{- "\n" }}
50
+ {{- tool | tojson }}
51
+ {%- endfor %}
52
+ {{- "\n</tools>" }}
53
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
+ {%- if messages[0].role == 'system' %}
55
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
56
+ {%- if content %}
57
+ {{- '\n\n' + content }}
58
+ {%- endif %}
59
+ {%- endif %}
60
+ {{- '<|im_end|>\n' }}
61
+ {%- else %}
62
+ {%- if messages[0].role == 'system' %}
63
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
64
+ {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
+ {%- for message in messages[::-1] %}
69
+ {%- set index = (messages|length - 1) - loop.index0 %}
70
+ {%- if ns.multi_step_tool and message.role == "user" %}
71
+ {%- set content = render_content(message.content, false)|trim %}
72
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
+ {%- set ns.multi_step_tool = false %}
74
+ {%- set ns.last_query_index = index %}
75
+ {%- endif %}
76
+ {%- endif %}
77
+ {%- endfor %}
78
+ {%- if ns.multi_step_tool %}
79
+ {{- raise_exception('No user query found in messages.') }}
80
+ {%- endif %}
81
+ {%- for message in messages %}
82
+ {%- set content = render_content(message.content, true)|trim %}
83
+ {%- if message.role == "system" %}
84
+ {%- if not loop.first %}
85
+ {{- raise_exception('System message must be at the beginning.') }}
86
+ {%- endif %}
87
+ {%- elif message.role == "user" %}
88
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
+ {%- elif message.role == "assistant" %}
90
+ {%- set reasoning_content = '' %}
91
+ {%- if message.reasoning_content is string %}
92
+ {%- set reasoning_content = message.reasoning_content %}
93
+ {%- else %}
94
+ {%- if '</think>' in content %}
95
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- set reasoning_content = reasoning_content|trim %}
100
+ {%- if loop.index0 > ns.last_query_index %}
101
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
+ {%- else %}
103
+ {{- '<|im_start|>' + message.role + '\n' + content }}
104
+ {%- endif %}
105
+ {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
+ {%- for tool_call in message.tool_calls %}
107
+ {%- if tool_call.function is defined %}
108
+ {%- set tool_call = tool_call.function %}
109
+ {%- endif %}
110
+ {%- if loop.first %}
111
+ {%- if content|trim %}
112
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
+ {%- else %}
114
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
+ {%- endif %}
116
+ {%- else %}
117
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
+ {%- endif %}
119
+ {%- if tool_call.arguments is defined %}
120
+ {%- for args_name, args_value in tool_call.arguments|items %}
121
+ {{- '<parameter=' + args_name + '>\n' }}
122
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
+ {{- args_value }}
124
+ {{- '\n</parameter>\n' }}
125
+ {%- endfor %}
126
+ {%- endif %}
127
+ {{- '</function>\n</tool_call>' }}
128
+ {%- endfor %}
129
+ {%- endif %}
130
+ {{- '<|im_end|>\n' }}
131
+ {%- elif message.role == "tool" %}
132
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
133
+ {{- '<|im_start|>user' }}
134
+ {%- endif %}
135
+ {{- '\n<tool_response>\n' }}
136
+ {{- content }}
137
+ {{- '\n</tool_response>' }}
138
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
139
+ {{- '<|im_end|>\n' }}
140
+ {%- elif loop.last %}
141
+ {{- '<|im_end|>\n' }}
142
+ {%- endif %}
143
+ {%- else %}
144
+ {{- raise_exception('Unexpected message role.') }}
145
+ {%- endif %}
146
+ {%- endfor %}
147
+ {%- if add_generation_prompt %}
148
+ {{- '<|im_start|>assistant\n' }}
149
+ {%- if enable_thinking is defined and enable_thinking is true %}
150
+ {{- '<think>\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n\n</think>\n\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "Anime_Image2Prompt",
3
+ "architectures": [
4
+ "Qwen3_5ForConditionalGeneration"
5
+ ],
6
+ "dtype": "bfloat16",
7
+ "image_token_id": 248056,
8
+ "model_type": "qwen3_5",
9
+ "text_config": {
10
+ "attention_bias": false,
11
+ "attention_dropout": 0.0,
12
+ "attn_output_gate": true,
13
+ "bos_token_id": null,
14
+ "dtype": "bfloat16",
15
+ "eos_token_id": 248044,
16
+ "full_attention_interval": 4,
17
+ "head_dim": 256,
18
+ "hidden_act": "silu",
19
+ "hidden_size": 2048,
20
+ "initializer_range": 0.02,
21
+ "intermediate_size": 6144,
22
+ "layer_types": [
23
+ "linear_attention",
24
+ "linear_attention",
25
+ "linear_attention",
26
+ "full_attention",
27
+ "linear_attention",
28
+ "linear_attention",
29
+ "linear_attention",
30
+ "full_attention",
31
+ "linear_attention",
32
+ "linear_attention",
33
+ "linear_attention",
34
+ "full_attention",
35
+ "linear_attention",
36
+ "linear_attention",
37
+ "linear_attention",
38
+ "full_attention",
39
+ "linear_attention",
40
+ "linear_attention",
41
+ "linear_attention",
42
+ "full_attention",
43
+ "linear_attention",
44
+ "linear_attention",
45
+ "linear_attention",
46
+ "full_attention"
47
+ ],
48
+ "linear_conv_kernel_dim": 4,
49
+ "linear_key_head_dim": 128,
50
+ "linear_num_key_heads": 16,
51
+ "linear_num_value_heads": 16,
52
+ "linear_value_head_dim": 128,
53
+ "mamba_ssm_dtype": "float32",
54
+ "max_position_embeddings": 262144,
55
+ "mlp_only_layers": [],
56
+ "model_type": "qwen3_5_text",
57
+ "mtp_num_hidden_layers": 1,
58
+ "mtp_use_dedicated_embeddings": false,
59
+ "num_attention_heads": 8,
60
+ "num_hidden_layers": 24,
61
+ "num_key_value_heads": 2,
62
+ "pad_token_id": null,
63
+ "partial_rotary_factor": 0.25,
64
+ "rms_norm_eps": 1e-06,
65
+ "rope_parameters": {
66
+ "mrope_interleaved": true,
67
+ "mrope_section": [
68
+ 11,
69
+ 11,
70
+ 10
71
+ ],
72
+ "partial_rotary_factor": 0.25,
73
+ "rope_theta": 10000000,
74
+ "rope_type": "default"
75
+ },
76
+ "tie_word_embeddings": true,
77
+ "use_cache": true,
78
+ "vocab_size": 248320
79
+ },
80
+ "tie_word_embeddings": true,
81
+ "transformers_version": "5.14.1",
82
+ "use_cache": true,
83
+ "video_token_id": 248057,
84
+ "vision_config": {
85
+ "deepstack_visual_indexes": [],
86
+ "depth": 24,
87
+ "dtype": "bfloat16",
88
+ "hidden_act": "gelu_pytorch_tanh",
89
+ "hidden_size": 1024,
90
+ "in_channels": 3,
91
+ "initializer_range": 0.02,
92
+ "intermediate_size": 4096,
93
+ "model_type": "qwen3_5_vision",
94
+ "num_heads": 16,
95
+ "num_position_embeddings": 2304,
96
+ "out_hidden_size": 2048,
97
+ "patch_size": 16,
98
+ "spatial_merge_size": 2,
99
+ "temporal_patch_size": 2
100
+ },
101
+ "vision_end_token_id": 248054,
102
+ "vision_start_token_id": 248053
103
+ }
generation_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": 248046,
4
+ "pad_token_id": 248044,
5
+ "transformers_version": "5.14.1",
6
+ "use_cache": true
7
+ }
images/01-regen.png ADDED

Git LFS Details

  • SHA256: 07226f7a7f661ef0cb3ce5fbef78f5cc8ee6952a430db07a4661c1c0a2bd57e7
  • Pointer size: 132 Bytes
  • Size of remote file: 3.21 MB
images/01.png ADDED

Git LFS Details

  • SHA256: 2a6f2ddca81021930896da8c21d776419e2d2a0b5f60557fdb1991a91a3821e7
  • Pointer size: 132 Bytes
  • Size of remote file: 2.51 MB
images/02-regen.png ADDED

Git LFS Details

  • SHA256: abbeaaaf80623948a0980bc6f9ad062fef49babb2c803535735efc3d7425aa5a
  • Pointer size: 132 Bytes
  • Size of remote file: 3.21 MB
images/02.png ADDED

Git LFS Details

  • SHA256: 98819176c7d08ba1981ee039af36e8769ae46073a7f7e15186dd2b506773a0d4
  • Pointer size: 132 Bytes
  • Size of remote file: 2.24 MB
inference.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local inference for Anime_Image2Prompt.
2
+
3
+ The model and processor are loaded only from the directory containing this
4
+ file. No base model, adapter, or network download is required.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import torch
16
+ from PIL import Image, ImageOps
17
+ from transformers import AutoModelForMultimodalLM, AutoProcessor
18
+
19
+
20
+ MODEL_DIR = Path(__file__).resolve().parent
21
+ INFERENCE_CONFIG = MODEL_DIR / "inference_config.json"
22
+ TAG_FIELDS = ("general_tags", "character_tags", "copyright_tags")
23
+ REQUIRED_MODEL_FILES = (
24
+ "config.json",
25
+ "model.safetensors",
26
+ "processor_config.json",
27
+ "tokenizer.json",
28
+ "tokenizer_config.json",
29
+ "chat_template.jinja",
30
+ "inference_config.json",
31
+ )
32
+
33
+
34
+ def _validate_model_directory(model_dir: Path) -> None:
35
+ missing = [name for name in REQUIRED_MODEL_FILES if not (model_dir / name).is_file()]
36
+ if missing:
37
+ raise FileNotFoundError(
38
+ f"Incomplete Anime_Image2Prompt package; missing: {', '.join(missing)}"
39
+ )
40
+
41
+
42
+ def _select_runtime(device: str) -> tuple[str, torch.dtype]:
43
+ cuda_available = torch.cuda.is_available()
44
+ if device == "cuda" and not cuda_available:
45
+ raise RuntimeError(
46
+ "CUDA was requested but is unavailable. Install a CUDA-enabled PyTorch "
47
+ "build and verify the NVIDIA driver, or use --device cpu."
48
+ )
49
+
50
+ use_cuda = device == "cuda" or (device == "auto" and cuda_available)
51
+ if not use_cuda:
52
+ return "cpu", torch.float32
53
+
54
+ supports_bf16 = getattr(torch.cuda, "is_bf16_supported", lambda: False)()
55
+ return "cuda", torch.bfloat16 if supports_bf16 else torch.float16
56
+
57
+
58
+ def _parse_model_json(text: str) -> dict[str, list[str]]:
59
+ start = text.find("{")
60
+ if start < 0:
61
+ raise ValueError(f"The model did not return JSON. Raw output: {text!r}")
62
+
63
+ try:
64
+ payload, _ = json.JSONDecoder().raw_decode(text[start:])
65
+ except json.JSONDecodeError as error:
66
+ raise ValueError(f"The model returned invalid JSON. Raw output: {text!r}") from error
67
+
68
+ if not isinstance(payload, dict) or set(payload) != set(TAG_FIELDS):
69
+ actual = tuple(payload.keys()) if isinstance(payload, dict) else type(payload).__name__
70
+ raise ValueError(f"Unexpected model output fields: {actual!r}")
71
+
72
+ normalized: dict[str, list[str]] = {}
73
+ for field in TAG_FIELDS:
74
+ value = payload[field]
75
+ if not isinstance(value, list) or len(value) > 1:
76
+ raise ValueError(f"{field} must be [] or a one-string list: {value!r}")
77
+ if value:
78
+ if not isinstance(value[0], str):
79
+ raise ValueError(f"{field} must contain a string: {value!r}")
80
+ tags = value[0].split(",")
81
+ if any(not tag for tag in tags):
82
+ raise ValueError(f"{field} contains an empty tag: {value!r}")
83
+ if len(tags) != len(set(tags)):
84
+ raise ValueError(f"{field} contains duplicate tags: {value!r}")
85
+ normalized[field] = value
86
+ return normalized
87
+
88
+
89
+ def _move_inputs(inputs: dict[str, Any], device: torch.device) -> dict[str, Any]:
90
+ return {
91
+ key: value.to(device) if hasattr(value, "to") else value
92
+ for key, value in inputs.items()
93
+ }
94
+
95
+
96
+ class AnimeImage2Prompt:
97
+ """Reusable local inference session for Anime_Image2Prompt."""
98
+
99
+ def __init__(self, device: str = "auto", low_vram: bool = False) -> None:
100
+ _validate_model_directory(MODEL_DIR)
101
+ runtime, dtype = _select_runtime(device)
102
+ self.runtime = runtime
103
+ self.dtype = dtype
104
+ self.config = json.loads(INFERENCE_CONFIG.read_text(encoding="utf-8"))
105
+
106
+ if runtime == "cuda":
107
+ device_map: str | dict[str, str] = "auto"
108
+ else:
109
+ device_map = {"": "cpu"}
110
+
111
+ self.processor = AutoProcessor.from_pretrained(
112
+ MODEL_DIR,
113
+ local_files_only=True,
114
+ )
115
+ if low_vram:
116
+ pixels = self.config["image_pixels"]
117
+ self.processor.image_processor.size = {
118
+ "shortest_edge": pixels["min"],
119
+ "longest_edge": pixels["low_vram_max"],
120
+ }
121
+
122
+ self.model = AutoModelForMultimodalLM.from_pretrained(
123
+ MODEL_DIR,
124
+ dtype=dtype,
125
+ device_map=device_map,
126
+ attn_implementation="sdpa",
127
+ local_files_only=True,
128
+ ).eval()
129
+
130
+ @property
131
+ def device(self) -> torch.device:
132
+ return self.model.device
133
+
134
+ def predict(
135
+ self,
136
+ image: str | Path | Image.Image,
137
+ max_new_tokens: int = 1024,
138
+ ) -> dict[str, list[str]]:
139
+ """Generate structured prompt tags for one image."""
140
+ if isinstance(image, Image.Image):
141
+ prepared_image = ImageOps.exif_transpose(image).convert("RGB")
142
+ else:
143
+ image_path = Path(image).expanduser().resolve()
144
+ if not image_path.is_file():
145
+ raise FileNotFoundError(f"Input image does not exist: {image_path}")
146
+ with Image.open(image_path) as source:
147
+ prepared_image = ImageOps.exif_transpose(source).convert("RGB")
148
+ prepared_image.load()
149
+
150
+ messages = [
151
+ {
152
+ "role": "system",
153
+ "content": [{"type": "text", "text": self.config["system_prompt"]}],
154
+ },
155
+ {
156
+ "role": "user",
157
+ "content": [
158
+ {"type": "image", "image": prepared_image},
159
+ {"type": "text", "text": self.config["user_prompt"]},
160
+ ],
161
+ },
162
+ ]
163
+ inputs = self.processor.apply_chat_template(
164
+ messages,
165
+ tokenize=True,
166
+ add_generation_prompt=True,
167
+ enable_thinking=False,
168
+ return_dict=True,
169
+ return_tensors="pt",
170
+ )
171
+ inputs = _move_inputs(inputs, self.device)
172
+
173
+ with torch.inference_mode():
174
+ output_ids = self.model.generate(
175
+ **inputs,
176
+ max_new_tokens=max_new_tokens,
177
+ do_sample=False,
178
+ eos_token_id=self.config["assistant_end_token_id"],
179
+ pad_token_id=self.processor.tokenizer.pad_token_id,
180
+ )
181
+
182
+ prompt_length = inputs["input_ids"].shape[1]
183
+ generated_ids = output_ids[:, prompt_length:]
184
+ raw_text = self.processor.batch_decode(
185
+ generated_ids,
186
+ skip_special_tokens=True,
187
+ clean_up_tokenization_spaces=False,
188
+ )[0]
189
+ return _parse_model_json(raw_text)
190
+
191
+ @staticmethod
192
+ def to_prompt(payload: dict[str, list[str]]) -> str:
193
+ """Flatten structured output to a comma-and-space-separated prompt."""
194
+ tags: list[str] = []
195
+ for field in TAG_FIELDS:
196
+ values = payload.get(field, [])
197
+ if values:
198
+ tags.extend(values[0].split(","))
199
+ return ", ".join(tags)
200
+
201
+
202
+ def _parse_args() -> argparse.Namespace:
203
+ parser = argparse.ArgumentParser(
204
+ description="Generate Danbooru-style prompt tags with Anime_Image2Prompt."
205
+ )
206
+ parser.add_argument("image", type=Path, help="path to one input image")
207
+ parser.add_argument(
208
+ "--output",
209
+ type=Path,
210
+ help="optional output file; the result is always printed to stdout",
211
+ )
212
+ parser.add_argument(
213
+ "--format",
214
+ choices=("json", "prompt"),
215
+ default="json",
216
+ help="output format (default: json)",
217
+ )
218
+ parser.add_argument(
219
+ "--device",
220
+ choices=("auto", "cuda", "cpu"),
221
+ default="auto",
222
+ help="inference device (default: auto)",
223
+ )
224
+ parser.add_argument(
225
+ "--low-vram",
226
+ action="store_true",
227
+ help="reduce image tokens to lower peak VRAM usage",
228
+ )
229
+ parser.add_argument(
230
+ "--max-new-tokens",
231
+ type=int,
232
+ default=1024,
233
+ help="maximum generated tokens (default: 1024)",
234
+ )
235
+ return parser.parse_args()
236
+
237
+
238
+ def main() -> int:
239
+ args = _parse_args()
240
+ if args.max_new_tokens < 1:
241
+ raise ValueError("--max-new-tokens must be greater than zero")
242
+
243
+ runtime, dtype = _select_runtime(args.device)
244
+ if runtime == "cuda":
245
+ hardware = torch.cuda.get_device_name(0)
246
+ print(f"Loading Anime_Image2Prompt on {hardware} ({dtype})...", file=sys.stderr)
247
+ else:
248
+ print("Loading Anime_Image2Prompt on CPU (this may be slow)...", file=sys.stderr)
249
+
250
+ tagger = AnimeImage2Prompt(device=args.device, low_vram=args.low_vram)
251
+ print("Generating prompt...", file=sys.stderr)
252
+ result = tagger.predict(args.image, max_new_tokens=args.max_new_tokens)
253
+
254
+ if args.format == "json":
255
+ rendered = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
256
+ else:
257
+ rendered = tagger.to_prompt(result) + "\n"
258
+
259
+ if args.output:
260
+ output_path = args.output.expanduser().resolve()
261
+ output_path.parent.mkdir(parents=True, exist_ok=True)
262
+ output_path.write_text(rendered, encoding="utf-8", newline="\n")
263
+ print(f"Saved: {output_path}", file=sys.stderr)
264
+
265
+ print(rendered, end="")
266
+ return 0
267
+
268
+
269
+ if __name__ == "__main__":
270
+ try:
271
+ raise SystemExit(main())
272
+ except (FileNotFoundError, RuntimeError, ValueError) as error:
273
+ print(f"Error: {error}", file=sys.stderr)
274
+ raise SystemExit(1) from error
inference_config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "Anime_Image2Prompt",
3
+ "task": "anime_image_to_prompt",
4
+ "system_prompt": "You are an image tagging model. Analyze the image and return only one valid JSON object. Do not add Markdown, explanations, comments, or hidden reasoning. Preserve tag spelling. For each non-empty category, use a JSON array containing exactly one string and join all unique tags inside that string with commas and no spaces after commas. Use an empty array for an empty category. Never repeat a tag. Close every JSON array and the JSON object, then stop immediately.",
5
+ "user_prompt": "Identify the Danbooru tags visible or attributable to this image. Return exactly these three keys: general_tags, character_tags, copyright_tags. Put all tags for each non-empty category in one comma-separated string inside a single-element JSON array. Use an empty array when a category cannot be identified.",
6
+ "tag_fields": [
7
+ "general_tags",
8
+ "character_tags",
9
+ "copyright_tags"
10
+ ],
11
+ "assistant_end_token": "<|im_end|>",
12
+ "assistant_end_token_id": 248046,
13
+ "image_pixels": {
14
+ "min": 200704,
15
+ "max": 401408,
16
+ "low_vram_max": 301056
17
+ }
18
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e7b265c171b46ee7e3dd4809b3b1cfc933c40cbdbe5c976f499e654daa0be3b
3
+ size 4426558864
processor_config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor": {
3
+ "do_convert_rgb": true,
4
+ "do_normalize": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_processor_type": "Qwen2VLImageProcessor",
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "merge_size": 2,
19
+ "patch_size": 16,
20
+ "resample": 3,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": {
23
+ "longest_edge": 401408,
24
+ "shortest_edge": 200704
25
+ },
26
+ "temporal_patch_size": 2
27
+ },
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "video_processor": {
30
+ "do_convert_rgb": true,
31
+ "do_normalize": true,
32
+ "do_rescale": true,
33
+ "do_resize": true,
34
+ "do_sample_frames": true,
35
+ "fps": 2,
36
+ "image_mean": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_std": [
42
+ 0.5,
43
+ 0.5,
44
+ 0.5
45
+ ],
46
+ "max_frames": 768,
47
+ "merge_size": 2,
48
+ "min_frames": 4,
49
+ "patch_size": 16,
50
+ "resample": 3,
51
+ "rescale_factor": 0.00392156862745098,
52
+ "return_metadata": false,
53
+ "size": {
54
+ "longest_edge": 25165824,
55
+ "shortest_edge": 4096
56
+ },
57
+ "temporal_patch_size": 2,
58
+ "video_processor_type": "Qwen3VLVideoProcessor"
59
+ }
60
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.6.0
2
+ transformers>=5.14.1,<6.0.0
3
+ accelerate>=1.6.0
4
+ pillow>=10.0.0
5
+ safetensors>=0.5.0
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
+ size 19989325
tokenizer_config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|im_end|>",
10
+ "errors": "replace",
11
+ "image_token": "<|image_pad|>",
12
+ "is_local": true,
13
+ "local_files_only": false,
14
+ "max_pixels": 401408,
15
+ "min_pixels": 200704,
16
+ "model_max_length": 262144,
17
+ "model_specific_special_tokens": {
18
+ "audio_bos_token": "<|audio_start|>",
19
+ "audio_eos_token": "<|audio_end|>",
20
+ "audio_token": "<|audio_pad|>",
21
+ "image_token": "<|image_pad|>",
22
+ "video_token": "<|video_pad|>",
23
+ "vision_bos_token": "<|vision_start|>",
24
+ "vision_eos_token": "<|vision_end|>"
25
+ },
26
+ "pad_token": "<|endoftext|>",
27
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "split_special_tokens": false,
30
+ "tokenizer_class": "Qwen2Tokenizer",
31
+ "unk_token": null,
32
+ "video_token": "<|video_pad|>",
33
+ "vision_bos_token": "<|vision_start|>",
34
+ "vision_eos_token": "<|vision_end|>"
35
+ }