koishi-plugin-memesluna 0.4.0 → 0.4.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/lib/index.js +84 -17
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -39,6 +39,7 @@ __export(index_exports, {
|
|
|
39
39
|
name: () => name
|
|
40
40
|
});
|
|
41
41
|
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
var import_fs = require("fs");
|
|
42
43
|
var import_path2 = __toESM(require("path"));
|
|
43
44
|
var import_koishi3 = require("koishi");
|
|
44
45
|
|
|
@@ -63,15 +64,25 @@ function hashImageBuffer(buffer) {
|
|
|
63
64
|
return (0, import_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
64
65
|
}
|
|
65
66
|
__name(hashImageBuffer, "hashImageBuffer");
|
|
66
|
-
function
|
|
67
|
+
function loadJimp() {
|
|
67
68
|
try {
|
|
68
|
-
const
|
|
69
|
+
const jimpModule = require("jimp");
|
|
70
|
+
return jimpModule.Jimp || jimpModule.default?.Jimp || null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
__name(loadJimp, "loadJimp");
|
|
76
|
+
function loadOptionalSharp() {
|
|
77
|
+
try {
|
|
78
|
+
const packageName = "sharp";
|
|
79
|
+
const sharpModule = require(packageName);
|
|
69
80
|
return sharpModule.default || sharpModule;
|
|
70
81
|
} catch {
|
|
71
82
|
return null;
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
|
-
__name(
|
|
85
|
+
__name(loadOptionalSharp, "loadOptionalSharp");
|
|
75
86
|
function countHexBitDistance(a, b) {
|
|
76
87
|
if (!a || !b || a.length !== b.length) return 64;
|
|
77
88
|
let distance = 0;
|
|
@@ -83,6 +94,47 @@ function countHexBitDistance(a, b) {
|
|
|
83
94
|
}
|
|
84
95
|
__name(countHexBitDistance, "countHexBitDistance");
|
|
85
96
|
var DHASH_BITS = 64;
|
|
97
|
+
var DHASH_WIDTH = 9;
|
|
98
|
+
var DHASH_HEIGHT = 8;
|
|
99
|
+
function lumaFromRgba(data, offset) {
|
|
100
|
+
const alpha = data[offset + 3] / 255;
|
|
101
|
+
const r = data[offset] * alpha + 255 * (1 - alpha);
|
|
102
|
+
const g = data[offset + 1] * alpha + 255 * (1 - alpha);
|
|
103
|
+
const b = data[offset + 2] * alpha + 255 * (1 - alpha);
|
|
104
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
105
|
+
}
|
|
106
|
+
__name(lumaFromRgba, "lumaFromRgba");
|
|
107
|
+
function averageCellLuma(data, width, height, cellX, cellY) {
|
|
108
|
+
const xStart = Math.floor(cellX * width / DHASH_WIDTH);
|
|
109
|
+
const xEnd = Math.max(xStart + 1, Math.floor((cellX + 1) * width / DHASH_WIDTH));
|
|
110
|
+
const yStart = Math.floor(cellY * height / DHASH_HEIGHT);
|
|
111
|
+
const yEnd = Math.max(yStart + 1, Math.floor((cellY + 1) * height / DHASH_HEIGHT));
|
|
112
|
+
let total = 0;
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (let y = yStart; y < Math.min(yEnd, height); y++) {
|
|
115
|
+
for (let x = xStart; x < Math.min(xEnd, width); x++) {
|
|
116
|
+
total += lumaFromRgba(data, (y * width + x) * 4);
|
|
117
|
+
count++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return count ? total / count : 0;
|
|
121
|
+
}
|
|
122
|
+
__name(averageCellLuma, "averageCellLuma");
|
|
123
|
+
function buildDhashFromLumaGrid(samples) {
|
|
124
|
+
let bits = "";
|
|
125
|
+
for (let y = 0; y < DHASH_HEIGHT; y++) {
|
|
126
|
+
const row = y * DHASH_WIDTH;
|
|
127
|
+
for (let x = 0; x < DHASH_WIDTH - 1; x++) {
|
|
128
|
+
bits += samples[row + x] > samples[row + x + 1] ? "1" : "0";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let hex = "";
|
|
132
|
+
for (let i = 0; i < bits.length; i += 4) {
|
|
133
|
+
hex += Number.parseInt(bits.slice(i, i + 4), 2).toString(16);
|
|
134
|
+
}
|
|
135
|
+
return hex;
|
|
136
|
+
}
|
|
137
|
+
__name(buildDhashFromLumaGrid, "buildDhashFromLumaGrid");
|
|
86
138
|
var RESERVED_PATHS = /* @__PURE__ */ new Set([
|
|
87
139
|
"config",
|
|
88
140
|
"admin",
|
|
@@ -305,7 +357,24 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
305
357
|
await this.backfillImageFingerprints();
|
|
306
358
|
}
|
|
307
359
|
async getImagePerceptualHash(buffer) {
|
|
308
|
-
const
|
|
360
|
+
const Jimp = loadJimp();
|
|
361
|
+
if (Jimp) {
|
|
362
|
+
try {
|
|
363
|
+
const image = await Jimp.read(buffer);
|
|
364
|
+
const { data, width, height } = image.bitmap;
|
|
365
|
+
if (!data || !width || !height) return "";
|
|
366
|
+
const samples = [];
|
|
367
|
+
for (let y = 0; y < DHASH_HEIGHT; y++) {
|
|
368
|
+
for (let x = 0; x < DHASH_WIDTH; x++) {
|
|
369
|
+
samples.push(averageCellLuma(data, width, height, x, y));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return buildDhashFromLumaGrid(samples);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
this.ctx.logger("memesluna").debug(`Jimp failed to calculate perceptual hash: ${error.message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const sharp = loadOptionalSharp();
|
|
309
378
|
if (!sharp) return "";
|
|
310
379
|
try {
|
|
311
380
|
const raw = await sharp(buffer, { animated: false, failOn: "none" }).resize(9, 8, { fit: "fill" }).grayscale().raw().toBuffer();
|
|
@@ -408,18 +477,17 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
408
477
|
}
|
|
409
478
|
async getSimilarStagedImages(threshold = this.config.similarityThreshold || 0.9) {
|
|
410
479
|
const normalizedThreshold = Math.min(1, Math.max(0.5, Number(threshold) || 0.9));
|
|
411
|
-
|
|
412
|
-
|
|
480
|
+
await this.backfillImageFingerprints();
|
|
481
|
+
const rows = await this.ctx.database.get("memesluna_staged_images", {});
|
|
482
|
+
const items = rows.map((row) => this.mapStagedImage(row)).filter((item) => item.perceptualHash);
|
|
483
|
+
if (!items.length) {
|
|
413
484
|
return {
|
|
414
|
-
available:
|
|
485
|
+
available: true,
|
|
415
486
|
threshold: normalizedThreshold,
|
|
416
487
|
groups: [],
|
|
417
|
-
message: "
|
|
488
|
+
message: rows.length ? "暂缓区图片暂未生成可比较的感知哈希" : "暂缓区暂无图片"
|
|
418
489
|
};
|
|
419
490
|
}
|
|
420
|
-
await this.backfillImageFingerprints();
|
|
421
|
-
const rows = await this.ctx.database.get("memesluna_staged_images", {});
|
|
422
|
-
const items = rows.map((row) => this.mapStagedImage(row)).filter((item) => item.perceptualHash);
|
|
423
491
|
const parent = /* @__PURE__ */ new Map();
|
|
424
492
|
const groupSimilarity = /* @__PURE__ */ new Map();
|
|
425
493
|
const find = /* @__PURE__ */ __name((id) => {
|
|
@@ -1413,15 +1481,15 @@ async function updateMemesVariable(ctx, config, service) {
|
|
|
1413
1481
|
}
|
|
1414
1482
|
__name(updateMemesVariable, "updateMemesVariable");
|
|
1415
1483
|
function applyConsole(ctx, config, service) {
|
|
1416
|
-
console.log("[MemesLuna] applyConsole started!");
|
|
1417
1484
|
if (!ctx.console) {
|
|
1418
|
-
console.log("[MemesLuna] ctx.console is missing!");
|
|
1419
1485
|
return;
|
|
1420
1486
|
}
|
|
1421
1487
|
const consoleService = ctx.console;
|
|
1422
|
-
const
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1488
|
+
const packageBase = import_path2.default.resolve(__dirname, "..");
|
|
1489
|
+
const installedBase = import_path2.default.resolve(ctx.baseDir, "node_modules", "koishi-plugin-memesluna");
|
|
1490
|
+
const consoleBase = (0, import_fs.existsSync)(installedBase) ? installedBase : packageBase;
|
|
1491
|
+
const devPath = import_path2.default.resolve(consoleBase, "client/index.ts");
|
|
1492
|
+
const prodPath = import_path2.default.resolve(consoleBase, "dist");
|
|
1425
1493
|
const withReady = /* @__PURE__ */ __name((handler) => {
|
|
1426
1494
|
return async (...args) => {
|
|
1427
1495
|
await service.ready;
|
|
@@ -1914,7 +1982,6 @@ function apply(ctx, config) {
|
|
|
1914
1982
|
applyServer(ctx2, config, service);
|
|
1915
1983
|
});
|
|
1916
1984
|
ctx.inject(["memesluna", "console"], async (ctx2) => {
|
|
1917
|
-
console.log("[MemesLuna] Console inject hook triggered!");
|
|
1918
1985
|
const service = ctx2.memesluna;
|
|
1919
1986
|
applyConsole(ctx2, config, service);
|
|
1920
1987
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-memesluna",
|
|
3
3
|
"description": "Image Forward service for Koishi with ChatLuna integration",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"node": ">=18.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"jimp": "^1.6.1",
|
|
44
45
|
"koishi-plugin-chatluna": "^1.3.0-alpha.49",
|
|
45
|
-
"koishi-plugin-chatluna-storage-service": "^1.0.6"
|
|
46
|
-
"sharp": "^0.34.5"
|
|
46
|
+
"koishi-plugin-chatluna-storage-service": "^1.0.6"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@koishijs/client": "^5.30.11",
|