Skip to content

Commit 5b1f77c

Browse files
committed
插件:AnimeTrace 动画/Galgame识别
1 parent e6e6dd4 commit 5b1f77c

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,18 @@ print("run[CQ:image,file="+j["img"]+"]")
392392

393393
- [x] waifu | 随机waifu(从[100000个AI生成的waifu](https://www.thiswaifudoesnotexist.net/)中随机一位)
394394

395+
</details>
396+
<details>
397+
<summary>AnimeTrace 动画/Galgame识别</summary>
398+
399+
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/animetrace"`
400+
401+
基于[AnimeTrace](https://ai.animedb.cn/)API 的识图搜索插件
402+
403+
- [x] Gal识图 | Gal识图 [模型名]
404+
405+
- [x] 动漫识图 | 动漫识图 2 | 动漫识图 [模型名]
406+
395407
</details>
396408
<details>
397409
<summary>支付宝到账语音</summary>

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import (
6767
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aifalse" // 服务器监控
6868
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiwife" // 随机老婆
6969
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音
70+
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/animetrace" // AnimeTrace 动画/Galgame识别
7071
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回
7172
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核
7273
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密

plugin/animetrace/main.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package animetrace
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"image"
9+
"image/jpeg"
10+
"image/png"
11+
"io"
12+
"mime/multipart"
13+
"net/http"
14+
"strings"
15+
"time"
16+
17+
ctrl "github.com/FloatTech/zbpctrl"
18+
"github.com/FloatTech/zbputils/control"
19+
"github.com/FloatTech/zbputils/ctxext"
20+
"github.com/tidwall/gjson"
21+
zero "github.com/wdvxdr1123/ZeroBot"
22+
"github.com/wdvxdr1123/ZeroBot/message"
23+
)
24+
25+
var (
26+
model string
27+
)
28+
29+
func init() {
30+
engine := control.Register("animetrace", &ctrl.Options[*zero.Ctx]{
31+
DisableOnDefault: false,
32+
Brief: "AnimeTrace 动画/Galgame识别",
33+
Help: "- Gal识图\n- 动漫识图\n- 动漫识图 2\n- 动漫识图 [模型名]\n- Gal识图 [模型名]",
34+
PublicDataFolder: "Animetrace",
35+
OnEnable: func(ctx *zero.Ctx) { ctx.Send("插件已启用") },
36+
OnDisable: func(ctx *zero.Ctx) { ctx.Send("插件已禁用") },
37+
})
38+
39+
engine.OnPrefix("gal识图", zero.OnlyGroup, zero.MustProvidePicture).SetBlock(true).Handle(func(ctx *zero.Ctx) {
40+
args := ctx.State["args"].(string)
41+
if strings.TrimSpace(args) == "" {
42+
model = "full_game_model_kira" // 默认使用的模型
43+
} else {
44+
model = args // 自定义设置模型
45+
}
46+
processImageRecognition(ctx, model)
47+
})
48+
49+
engine.OnPrefix("动漫识图", zero.OnlyGroup, zero.MustProvidePicture).SetBlock(true).Handle(func(ctx *zero.Ctx) {
50+
args := ctx.State["args"].(string)
51+
if strings.TrimSpace(args) == "" {
52+
model = "anime_model_lovelive"
53+
} else if strings.TrimSpace(args) == "2" {
54+
model = "pre_stable"
55+
} else {
56+
model = args
57+
}
58+
processImageRecognition(ctx, model)
59+
})
60+
}
61+
62+
// 处理图片识别
63+
func processImageRecognition(ctx *zero.Ctx, model string) {
64+
urls := ctx.State["image_url"].([]string)
65+
if len(urls) == 0 {
66+
return
67+
}
68+
69+
imageData, err := downloadImage(urls[0])
70+
if err != nil {
71+
ctx.Send(message.Text("下载图片失败: ", err))
72+
return
73+
}
74+
//ctx.Send(message.Text(model))
75+
respBody, err := createAndSendMultipartRequest("https://api.animetrace.com/v1/search", imageData, map[string]string{
76+
"is_multi": "0",
77+
"model": model,
78+
"ai_detect": "0",
79+
})
80+
if err != nil {
81+
ctx.Send(message.Text("识别请求失败: ", err))
82+
return
83+
}
84+
85+
code := gjson.Get(string(respBody), "code").Int()
86+
if code != 0 {
87+
ctx.Send(message.Text("错误: ", gjson.Get(string(respBody), "zh_message").String()))
88+
return
89+
}
90+
91+
dataArray := gjson.Get(string(respBody), "data").Array()
92+
countStr := fmt.Sprintf("%d", len(dataArray))
93+
if countStr == "0" {
94+
ctx.Send(message.Text("未识别到任何角色"))
95+
return
96+
}
97+
var sk message.Message
98+
sk = append(sk, ctxext.FakeSenderForwardNode(ctx, message.Text("共识别到 "+countStr+" 个角色,可能是以下来源")))
99+
100+
for _, value := range dataArray {
101+
boxArray := value.Get("box").Array()
102+
box := make([]float64, len(boxArray))
103+
for i, val := range boxArray {
104+
box[i] = val.Float()
105+
}
106+
base64Str, err := cropImage(imageData, box)
107+
if err != nil {
108+
ctx.Send(message.Text("图片处理失败: ", err))
109+
return
110+
}
111+
var sb strings.Builder
112+
value.Get("character").ForEach(func(_, character gjson.Result) bool {
113+
sb.WriteString(fmt.Sprintf("《%s》的角色 %s\n", character.Get("work").String(), character.Get("character").String()))
114+
return true
115+
})
116+
117+
sk = append(sk, ctxext.FakeSenderForwardNode(ctx, message.Image("base64://"+base64Str), message.Text(sb.String())))
118+
}
119+
120+
ctx.SendGroupForwardMessage(ctx.Event.GroupID, sk)
121+
}
122+
123+
// 下载图片,设置超时
124+
func downloadImage(url string) ([]byte, error) {
125+
client := &http.Client{Timeout: 10 * time.Second}
126+
resp, err := client.Get(url)
127+
if err != nil {
128+
return nil, fmt.Errorf("下载失败: %v", err)
129+
}
130+
defer resp.Body.Close()
131+
132+
if resp.StatusCode != http.StatusOK {
133+
return nil, fmt.Errorf("状态码: %d", resp.StatusCode)
134+
}
135+
136+
return io.ReadAll(resp.Body)
137+
}
138+
139+
// 发送图片识别请求
140+
func createAndSendMultipartRequest(url string, imageData []byte, formFields map[string]string) ([]byte, error) {
141+
body := &bytes.Buffer{}
142+
writer := multipart.NewWriter(body)
143+
144+
part, err := writer.CreateFormFile("file", "image.jpg")
145+
if err != nil {
146+
return nil, fmt.Errorf("创建文件字段失败: %v", err)
147+
}
148+
if _, err := io.Copy(part, bytes.NewReader(imageData)); err != nil {
149+
return nil, fmt.Errorf("写入文件数据失败: %v", err)
150+
}
151+
152+
for key, value := range formFields {
153+
_ = writer.WriteField(key, value)
154+
}
155+
writer.Close()
156+
157+
client := &http.Client{Timeout: 15 * time.Second}
158+
resp, err := client.Post(url, writer.FormDataContentType(), body)
159+
if err != nil {
160+
return nil, fmt.Errorf("请求失败: %v", err)
161+
}
162+
defer resp.Body.Close()
163+
return io.ReadAll(resp.Body)
164+
}
165+
166+
// 裁剪图片并返回 Base64
167+
func cropImage(buffer []byte, box []float64) (string, error) {
168+
contentType := http.DetectContentType(buffer)
169+
if contentType != "image/jpeg" && contentType != "image/png" {
170+
return "", fmt.Errorf("不支持的图片格式: %s", contentType)
171+
}
172+
173+
img, format, err := image.Decode(bytes.NewReader(buffer))
174+
if err != nil {
175+
return "", fmt.Errorf("解码失败: %v", err)
176+
}
177+
178+
bounds := img.Bounds()
179+
width, height := bounds.Dx(), bounds.Dy()
180+
cropX, cropY := int(float64(width)*box[0]), int(float64(height)*box[1])
181+
cropW, cropH := int(float64(width)*(box[2]-box[0])), int(float64(height)*(box[3]-box[1]))
182+
183+
if cropW <= 0 || cropH <= 0 {
184+
return "", errors.New("裁剪区域无效")
185+
}
186+
187+
croppedImg := img.(interface {
188+
SubImage(r image.Rectangle) image.Image
189+
}).SubImage(image.Rect(cropX, cropY, cropX+cropW, cropY+cropH))
190+
191+
var buf bytes.Buffer
192+
switch format {
193+
case "jpeg":
194+
err = jpeg.Encode(&buf, croppedImg, nil)
195+
case "png":
196+
err = png.Encode(&buf, croppedImg)
197+
default:
198+
return "", fmt.Errorf("不支持的格式: %s", format)
199+
}
200+
if err != nil {
201+
return "", fmt.Errorf("图片编码失败: %v", err)
202+
}
203+
204+
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
205+
}

0 commit comments

Comments
 (0)