kokkoro-plugin-animetrace 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/package.json +1 -1
- package/src/index.ts +11 -13
- package/src/service.ts +21 -11
- package/src/util.ts +25 -10
package/README.md
CHANGED
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
bun add kokkoro-plugin-animetrace
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
发送「/搜角色」指令时附带图片,或引用包含图片的消息回复该指令。插件识别首张图片中的角色,并通过 QQ Markdown 返回结果。「搜角色」与「/搜角色」的用法相同。
|
|
12
12
|
|
|
13
13
|
完整使用说明见 [AnimeTrace 角色识别](https://kokkoro.js.org/plugin/animetrace)。
|
|
14
14
|
|
|
15
|
+
## 环境变量
|
|
16
|
+
|
|
17
|
+
在项目根目录的 `.env` 中配置每个人物最多显示的候选角色数量:
|
|
18
|
+
|
|
19
|
+
```ini
|
|
20
|
+
ANIMETRACE_LIMIT=3
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`ANIMETRACE_LIMIT` 必须为正整数,默认值为 `3`。
|
|
24
|
+
|
|
15
25
|
## API
|
|
16
26
|
|
|
17
27
|
通过 `service` 入口导入 `fetchCharacters()` 获取完整响应,通过 `util` 入口导入 `createMarkdown()` 生成 QQ Markdown:
|
|
@@ -24,4 +34,6 @@ const { data } = await fetchCharacters('https://kokkoro.js.org/logo.png');
|
|
|
24
34
|
const markdown = createMarkdown(data);
|
|
25
35
|
```
|
|
26
36
|
|
|
37
|
+
`fetchCharacters()` 的第二个参数可传入日志记录器,记录完整请求参数和接口响应。
|
|
38
|
+
|
|
27
39
|
`service` 入口同时导出 `ANIMETRACE_API`、`AnimeTrace` 和 `CharacterResult`。参数、响应字段和错误行为见 [插件 API 文档](https://kokkoro.js.org/plugin/animetrace#api)。
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { useCommand, useLogger } from '@kokkoro/core';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { fetchCharacters } from './service';
|
|
4
4
|
import { createMarkdown } from './util';
|
|
5
5
|
|
|
6
6
|
const logger = useLogger();
|
|
7
7
|
|
|
8
8
|
export default () => {
|
|
9
9
|
useCommand('/搜角色', async context => {
|
|
10
|
-
const
|
|
10
|
+
const [reference] = context.msg_elements ?? [];
|
|
11
|
+
const attachments = context.message_type === 103 ? reference?.attachments : context.attachments;
|
|
12
|
+
const images = attachments?.filter(attachment => attachment.content_type?.startsWith('image/')) ?? [];
|
|
13
|
+
const [image] = images;
|
|
11
14
|
|
|
12
15
|
if (!image?.url) {
|
|
13
|
-
throw new Error('
|
|
16
|
+
throw new Error('未检测到图片,请在指令消息中附带图片,或引用包含图片的消息后重试。');
|
|
14
17
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
payload: { url: image.url, is_multi: 1, ai_detect: 0 },
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const result = await fetchCharacters(image.url);
|
|
18
|
+
const result = await fetchCharacters(image.url, logger);
|
|
19
|
+
const { data, trace_id } = result;
|
|
20
|
+
const notice = images.length > 1 ? '检测到消息中包含多张图片,已选取首张图片进行识别。' : undefined;
|
|
22
21
|
|
|
23
|
-
logger.
|
|
24
|
-
logger.info('已识别图片中的角色', { count: result.data.length, trace_id: result.trace_id });
|
|
22
|
+
logger.info('已识别图片中的角色', { count: data.length, trace_id });
|
|
25
23
|
|
|
26
24
|
await context.reply({
|
|
27
25
|
msg_type: 2,
|
|
28
|
-
markdown: { content: createMarkdown(
|
|
26
|
+
markdown: { content: createMarkdown(data, notice) },
|
|
29
27
|
});
|
|
30
28
|
}).shortcut('搜角色');
|
|
31
29
|
};
|
package/src/service.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type Logger } from '@kokkoro/core';
|
|
2
|
+
|
|
1
3
|
/** AnimeTrace v1 识别接口的请求地址。 */
|
|
2
4
|
export const ANIMETRACE_API = 'https://api.animetrace.com/v1/search';
|
|
3
5
|
|
|
@@ -11,9 +13,9 @@ export interface AnimeTrace {
|
|
|
11
13
|
readonly code: number;
|
|
12
14
|
/** 是否判定为 AI 生成图片。 */
|
|
13
15
|
readonly ai: boolean;
|
|
14
|
-
/** 本次识别的唯一 ID
|
|
16
|
+
/** 本次识别的唯一 ID,反馈问题时使用。 */
|
|
15
17
|
readonly trace_id: string;
|
|
16
|
-
/**
|
|
18
|
+
/** 检测到的每个人物的结果列表。 */
|
|
17
19
|
readonly data: readonly CharacterResult[];
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -21,11 +23,11 @@ export interface AnimeTrace {
|
|
|
21
23
|
export interface CharacterResult {
|
|
22
24
|
/** 人物位置 `[x1, y1, x2, y2]`,坐标相对于图片宽高,范围为 0 到 1。 */
|
|
23
25
|
readonly box: readonly [number, number, number, number];
|
|
24
|
-
/**
|
|
26
|
+
/** 每个检测框的唯一 ID。 */
|
|
25
27
|
readonly box_id: string;
|
|
26
|
-
/**
|
|
28
|
+
/** 置信度较低,候选较多,需人工确认。 */
|
|
27
29
|
readonly not_confident: boolean;
|
|
28
|
-
/**
|
|
30
|
+
/** 候选角色列表,越靠前可能性越大。 */
|
|
29
31
|
readonly character: readonly {
|
|
30
32
|
/** 作品名称。 */
|
|
31
33
|
readonly work: string;
|
|
@@ -35,26 +37,34 @@ export interface CharacterResult {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** 使用 AnimeTrace 默认模型识别图片中的角色,并返回完整的接口响应。 */
|
|
38
|
-
export async function fetchCharacters(url: string): Promise<AnimeTrace> {
|
|
40
|
+
export async function fetchCharacters(url: string, logger?: Logger): Promise<AnimeTrace> {
|
|
41
|
+
const payload = { url, is_multi: '1', ai_detect: '0' };
|
|
39
42
|
const form = new FormData();
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
45
|
+
form.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
logger?.debug('发送 AnimeTrace 请求', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
url: ANIMETRACE_API,
|
|
50
|
+
payload,
|
|
51
|
+
});
|
|
44
52
|
|
|
45
53
|
const response = await fetch(ANIMETRACE_API, { method: 'POST', body: form });
|
|
46
54
|
|
|
47
55
|
if (!response.ok) {
|
|
48
|
-
throw new Error(
|
|
56
|
+
throw new Error(`AnimeTrace 请求失败,HTTP 状态码 ${response.status}`);
|
|
49
57
|
}
|
|
50
58
|
const result = <AnimeTrace>await response.json();
|
|
51
59
|
|
|
60
|
+
logger?.debug('收到 AnimeTrace 响应', result);
|
|
61
|
+
|
|
52
62
|
if (result.code !== 0) {
|
|
53
63
|
throw new Error(`AnimeTrace 识别失败,状态码 ${result.code}`);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
if (result.data.length === 0) {
|
|
57
|
-
throw new Error('
|
|
67
|
+
throw new Error('未识别到角色');
|
|
58
68
|
}
|
|
59
69
|
return result;
|
|
60
70
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { type CharacterResult } from './service';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const { ANIMETRACE_LIMIT = '3' } = import.meta.env;
|
|
4
|
+
const limit = Number(ANIMETRACE_LIMIT);
|
|
5
|
+
|
|
6
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
7
|
+
throw new Error('ANIMETRACE_LIMIT 必须为正整数');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 将 AnimeTrace 识别结果转换为 QQ Markdown,可在标题下方展示提示。 */
|
|
11
|
+
export function createMarkdown(results: readonly CharacterResult[], notice?: string): string {
|
|
12
|
+
const lines = ['## AnimeTrace 识别结果'];
|
|
13
|
+
|
|
14
|
+
if (notice) {
|
|
15
|
+
lines.push('', `> ${notice}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (results.some(({ not_confident: isNotConfident }) => isNotConfident)) {
|
|
19
|
+
lines.push('', '> 标有 * 的识别结果可能不准确,请注意甄别。');
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
for (const [index, { character: candidates, not_confident: isNotConfident }] of results.entries()) {
|
|
8
|
-
|
|
9
|
-
lines.push(`**人物 ${index + 1}**`);
|
|
10
|
-
}
|
|
11
|
-
lines.push(candidates.map(({ character, work }) => `- **${character}** \n ${work}`).join('\n'));
|
|
23
|
+
const prefix = results.length === 1 && isNotConfident ? '* ' : '';
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
lines.push('');
|
|
26
|
+
|
|
27
|
+
if (results.length > 1) {
|
|
28
|
+
lines.push(`**${isNotConfident ? '* ' : ''}人物 ${index + 1}**`);
|
|
15
29
|
}
|
|
30
|
+
lines.push(...candidates.slice(0, limit).map(({ character, work }) => `- **${prefix}${character}** \n ${work}`));
|
|
16
31
|
}
|
|
17
|
-
return lines.join('\n
|
|
32
|
+
return lines.join('\n');
|
|
18
33
|
}
|