pixivflow 2.9.0 → 2.10.0
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 +2 -0
- package/dist/commands/TopicCommand.d.ts +19 -0
- package/dist/commands/TopicCommand.d.ts.map +1 -0
- package/dist/commands/TopicCommand.js +113 -0
- package/dist/commands/TopicCommand.js.map +1 -0
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/config/types.d.ts +45 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +40 -3
- package/dist/config/validation.js.map +1 -1
- package/dist/download/DownloadManager.d.ts.map +1 -1
- package/dist/download/DownloadManager.js +5 -2
- package/dist/download/DownloadManager.js.map +1 -1
- package/dist/download/handlers/IllustrationTargetHandler.d.ts +5 -1
- package/dist/download/handlers/IllustrationTargetHandler.d.ts.map +1 -1
- package/dist/download/handlers/IllustrationTargetHandler.js +26 -1
- package/dist/download/handlers/IllustrationTargetHandler.js.map +1 -1
- package/dist/download/handlers/NovelTargetHandler.d.ts +4 -1
- package/dist/download/handlers/NovelTargetHandler.d.ts.map +1 -1
- package/dist/download/handlers/NovelTargetHandler.js +23 -1
- package/dist/download/handlers/NovelTargetHandler.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/pixiv/PixivClient.d.ts +8 -2
- package/dist/pixiv/PixivClient.d.ts.map +1 -1
- package/dist/pixiv/PixivClient.js +6 -2
- package/dist/pixiv/PixivClient.js.map +1 -1
- package/dist/pixiv/types.d.ts +7 -0
- package/dist/pixiv/types.d.ts.map +1 -1
- package/dist/storage/Database.d.ts +2 -0
- package/dist/storage/Database.d.ts.map +1 -1
- package/dist/storage/Database.js +4 -0
- package/dist/storage/Database.js.map +1 -1
- package/dist/topic/TopicCache.d.ts +23 -0
- package/dist/topic/TopicCache.d.ts.map +1 -0
- package/dist/topic/TopicCache.js +78 -0
- package/dist/topic/TopicCache.js.map +1 -0
- package/dist/topic/TopicPipeline.d.ts +49 -0
- package/dist/topic/TopicPipeline.d.ts.map +1 -0
- package/dist/topic/TopicPipeline.js +224 -0
- package/dist/topic/TopicPipeline.js.map +1 -0
- package/dist/topic/TopicResolver.d.ts +31 -0
- package/dist/topic/TopicResolver.d.ts.map +1 -0
- package/dist/topic/TopicResolver.js +162 -0
- package/dist/topic/TopicResolver.js.map +1 -0
- package/dist/topic/TopicTagScorer.d.ts +13 -0
- package/dist/topic/TopicTagScorer.d.ts.map +1 -0
- package/dist/topic/TopicTagScorer.js +109 -0
- package/dist/topic/TopicTagScorer.js.map +1 -0
- package/dist/topic/createTopicPipeline.d.ts +12 -0
- package/dist/topic/createTopicPipeline.d.ts.map +1 -0
- package/dist/topic/createTopicPipeline.js +31 -0
- package/dist/topic/createTopicPipeline.js.map +1 -0
- package/dist/topic/types.d.ts +93 -0
- package/dist/topic/types.d.ts.map +1 -0
- package/dist/topic/types.js +8 -0
- package/dist/topic/types.js.map +1 -0
- package/dist/webui/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicPipeline.d.ts","sourceRoot":"","sources":["../../src/topic/TopicPipeline.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,EACT,MAAM,SAAS,CAAC;AAYjB,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,qBAAa,aAAa;IAEtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc;gBAFd,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,aAAa,EACvB,cAAc,SAAM;IAGjC,WAAW,CAAC,CAAC,SAAS,QAAQ,EAClC,MAAM,EAAE,YAAY,EACpB,WAAW,EAAE,gBAAgB,EAC7B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;QAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAAC,SAAS,EAAE,cAAc,CAAA;KAAE,CAAC;YAgEvC,SAAS;IAmBvB,OAAO,CAAC,WAAW;IAenB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAmCrB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,KAAK;CAKd"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TopicPipeline = void 0;
|
|
4
|
+
const promises_1 = require("node:timers/promises");
|
|
5
|
+
const logger_1 = require("../logger");
|
|
6
|
+
const pixiv_utils_1 = require("../utils/pixiv-utils");
|
|
7
|
+
const COLLECT_DEFAULTS = { maxPerTag: 40, maxCandidates: 250, minMetadataScore: 0.35 };
|
|
8
|
+
/** Small, platform-level tags that are never evidence of topic membership. */
|
|
9
|
+
const STOP_TAGS = new Set([
|
|
10
|
+
'r-18', 'r-18g', 'r-15', 'original', 'オリジナル', '創作', '女の子', '男の子',
|
|
11
|
+
'イラスト', '漫画', 'manga', 'illustration', 'artwork', '落書き', 'らくがき',
|
|
12
|
+
'1000users入り', '5000users入り', '10000users入り', '500users入り', '100users入り',
|
|
13
|
+
'pixiv', 'commission', 'skeb', '依頼絵', '仕事絵',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a topic to a tag space, collects that day's works across the tags,
|
|
17
|
+
* filters by lightweight metadata relevance and ranks by local popularity, then
|
|
18
|
+
* returns only the chosen full work objects for the existing download pipeline.
|
|
19
|
+
* Serial, bounded, low-memory: the retained set is `limit` items; the pool is
|
|
20
|
+
* capped at maxCandidates and released after selection.
|
|
21
|
+
*/
|
|
22
|
+
class TopicPipeline {
|
|
23
|
+
client;
|
|
24
|
+
resolver;
|
|
25
|
+
requestDelayMs;
|
|
26
|
+
constructor(client, resolver, requestDelayMs = 500) {
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.resolver = resolver;
|
|
29
|
+
this.requestDelayMs = requestDelayMs;
|
|
30
|
+
}
|
|
31
|
+
async selectWorks(target, contentType, day, limit, discovery, collect) {
|
|
32
|
+
const topic = (target.topic ?? '').trim();
|
|
33
|
+
const { space } = await this.resolver.resolve(topic, contentType, discovery);
|
|
34
|
+
const tagScores = new Map(space.tags.map((t) => [this.key(t.name), t.score]));
|
|
35
|
+
const maxPerTag = this.bound(collect.maxPerTag, COLLECT_DEFAULTS.maxPerTag, 5, 100);
|
|
36
|
+
const maxCandidates = this.bound(collect.maxCandidates, COLLECT_DEFAULTS.maxCandidates, 20, 500);
|
|
37
|
+
const minMetadataScore = collect.minMetadataScore ?? COLLECT_DEFAULTS.minMetadataScore;
|
|
38
|
+
const byId = new Map();
|
|
39
|
+
let rawCount = 0;
|
|
40
|
+
const tagNames = space.tags.map((t) => t.name);
|
|
41
|
+
for (let i = 0; i < tagNames.length; i++) {
|
|
42
|
+
if (byId.size >= maxCandidates)
|
|
43
|
+
break;
|
|
44
|
+
const tag = tagNames[i];
|
|
45
|
+
const works = await this.searchDay(contentType, tag, day, maxPerTag);
|
|
46
|
+
rawCount += works.length;
|
|
47
|
+
for (const work of works) {
|
|
48
|
+
if (byId.has(work.id))
|
|
49
|
+
continue;
|
|
50
|
+
byId.set(work.id, { work, candidate: this.toCandidate(work, contentType) });
|
|
51
|
+
if (byId.size >= maxCandidates)
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
logger_1.logger.debug('[TopicCollector] type=' + contentType + ' tag=' + tag + ' day=' + day + ' fetched=' + works.length + ' pool=' + byId.size);
|
|
55
|
+
if (i < tagNames.length - 1 && this.requestDelayMs > 0)
|
|
56
|
+
await (0, promises_1.setTimeout)(this.requestDelayMs);
|
|
57
|
+
}
|
|
58
|
+
const dedupedCount = byId.size;
|
|
59
|
+
logger_1.logger.info('[TopicCollector] type=' + contentType + ' raw=' + rawCount + ' deduplicated=' + dedupedCount);
|
|
60
|
+
const seedKey = this.key(topic);
|
|
61
|
+
const accepted = [];
|
|
62
|
+
for (const entry of byId.values()) {
|
|
63
|
+
entry.candidate.metadataScore = this.metadataScore(entry.candidate, seedKey, tagScores);
|
|
64
|
+
if (entry.candidate.metadataScore >= minMetadataScore)
|
|
65
|
+
accepted.push(entry);
|
|
66
|
+
}
|
|
67
|
+
if (accepted.length === 0 && byId.size > 0) {
|
|
68
|
+
const fallback = [...byId.values()]
|
|
69
|
+
.filter((e) => e.candidate.tags.some((t) => this.key(t) === seedKey))
|
|
70
|
+
.sort((a, b) => b.candidate.popularity - a.candidate.popularity);
|
|
71
|
+
accepted.push(...fallback.slice(0, Math.max(limit, 1)));
|
|
72
|
+
logger_1.logger.warn('[MetadataTopicFilter] type=' + contentType + ' none above threshold ' + minMetadataScore + '; kept ' + accepted.length + ' seed-tag fallback');
|
|
73
|
+
}
|
|
74
|
+
const chosen = this.topByPopularity(accepted, limit);
|
|
75
|
+
const selected = chosen.map((e) => e.candidate);
|
|
76
|
+
logger_1.logger.info('[MetadataTopicFilter] accepted=' + accepted.length);
|
|
77
|
+
if (selected[0]) {
|
|
78
|
+
logger_1.logger.info('[PopularityRanker] selected=' + selected[0].id + ' popularity=' + selected[0].popularity.toFixed(1) + ' meta=' + selected[0].metadataScore.toFixed(2) + ' title=' + selected[0].title);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
works: chosen.map((e) => e.work),
|
|
82
|
+
selection: {
|
|
83
|
+
candidates: [...byId.values()].map((e) => e.candidate),
|
|
84
|
+
selected,
|
|
85
|
+
resolvedTagCount: space.tags.length,
|
|
86
|
+
rawCount,
|
|
87
|
+
dedupedCount,
|
|
88
|
+
acceptedCount: accepted.length,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async searchDay(contentType, tag, day, limit) {
|
|
93
|
+
try {
|
|
94
|
+
const opts = { startDate: day, endDate: day };
|
|
95
|
+
const works = contentType === 'illustration'
|
|
96
|
+
? await this.client.searchIllustrationsForTags(tag, limit, opts)
|
|
97
|
+
: await this.client.searchNovelsForTags(tag, limit, opts);
|
|
98
|
+
// Date is already enforced server-side + pager stop; keep a cheap guard.
|
|
99
|
+
return works.filter((w) => this.onDay(w.create_date, day));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logger_1.logger.warn('[TopicCollector] search failed tag=' + tag + ' type=' + contentType + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
toCandidate(work, type) {
|
|
107
|
+
const popularity = (0, pixiv_utils_1.calculatePopularityScore)(work);
|
|
108
|
+
return {
|
|
109
|
+
id: work.id,
|
|
110
|
+
type,
|
|
111
|
+
title: work.title ?? '',
|
|
112
|
+
caption: work.caption ?? '',
|
|
113
|
+
tags: (work.tags ?? []).map((t) => t.name).filter(Boolean),
|
|
114
|
+
bookmarks: Number(work.total_bookmarks ?? work.bookmark_count ?? 0) || 0,
|
|
115
|
+
views: Number(work.total_view ?? work.view_count ?? 0) || 0,
|
|
116
|
+
popularity,
|
|
117
|
+
metadataScore: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Lightweight metadata relevance. Tags dominate (Pixiv's own taxonomy);
|
|
122
|
+
* title/caption add smaller boosts. The seed tag is strong evidence.
|
|
123
|
+
* No text model — case/symbol-insensitive substring matching only.
|
|
124
|
+
*/
|
|
125
|
+
metadataScore(candidate, seedKey, tagScores) {
|
|
126
|
+
let seedHit = false;
|
|
127
|
+
let relatedSum = 0;
|
|
128
|
+
let relatedHits = 0;
|
|
129
|
+
for (const tag of candidate.tags) {
|
|
130
|
+
const k = this.key(tag);
|
|
131
|
+
if (STOP_TAGS.has(k))
|
|
132
|
+
continue;
|
|
133
|
+
if (k === seedKey) {
|
|
134
|
+
seedHit = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const related = tagScores.get(k);
|
|
138
|
+
if (related !== undefined) {
|
|
139
|
+
relatedSum += Math.min(related, 0.6);
|
|
140
|
+
relatedHits += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const hayTitle = this.normalize(candidate.title);
|
|
144
|
+
const hayCaption = this.normalize(candidate.caption);
|
|
145
|
+
const titleSeed = !!seedKey && hayTitle.includes(seedKey);
|
|
146
|
+
const captionSeed = !!seedKey && hayCaption.includes(seedKey);
|
|
147
|
+
if (seedHit) {
|
|
148
|
+
// Core topic work: the seed tag itself is the strongest evidence.
|
|
149
|
+
let score = 1.0 + Math.min(relatedSum, 0.5);
|
|
150
|
+
if (titleSeed)
|
|
151
|
+
score += 0.4;
|
|
152
|
+
return score;
|
|
153
|
+
}
|
|
154
|
+
// No seed tag: the work must show several independent related tags (or a
|
|
155
|
+
// high-weight one plus text) to count as on-topic. A single marginal tag
|
|
156
|
+
// deliberately does NOT clear the bar, so a hugely popular tangential work
|
|
157
|
+
// cannot crowd out core topic works.
|
|
158
|
+
let score = 0;
|
|
159
|
+
const strongRelated = [...tagScores.entries()].some(([k, w]) => w >= 0.6 && candidate.tags.some((t) => this.key(t) === k));
|
|
160
|
+
if (titleSeed)
|
|
161
|
+
score += 0.8;
|
|
162
|
+
if (captionSeed)
|
|
163
|
+
score += 0.4;
|
|
164
|
+
if (strongRelated)
|
|
165
|
+
score += 0.5;
|
|
166
|
+
score += Math.min(relatedSum, 0.6) * (relatedHits >= 2 ? 1 : 0.5);
|
|
167
|
+
return score;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Rank primarily by topic relevance tier, then by local popularity within a
|
|
171
|
+
* tier. Relevance wins because the goal is "belongs to the topic", with
|
|
172
|
+
* popularity deciding which on-topic work to download.
|
|
173
|
+
*/
|
|
174
|
+
relevanceTier(c) {
|
|
175
|
+
if (c.metadataScore >= 1.0)
|
|
176
|
+
return 3; // carries the seed tag / strong evidence
|
|
177
|
+
if (c.metadataScore >= 0.7)
|
|
178
|
+
return 2;
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
rankCompare(a, b) {
|
|
182
|
+
const tier = this.relevanceTier(b) - this.relevanceTier(a);
|
|
183
|
+
if (tier !== 0)
|
|
184
|
+
return tier;
|
|
185
|
+
return b.popularity - a.popularity;
|
|
186
|
+
}
|
|
187
|
+
topByPopularity(items, limit) {
|
|
188
|
+
if (items.length <= limit)
|
|
189
|
+
return items.sort((a, b) => this.rankCompare(a.candidate, b.candidate));
|
|
190
|
+
// O(n) top-`limit` selection (limit is tiny, e.g. 1); avoids a full sort.
|
|
191
|
+
const top = [];
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
if (top.length < limit) {
|
|
194
|
+
top.push(item);
|
|
195
|
+
top.sort((a, b) => this.rankCompare(a.candidate, b.candidate));
|
|
196
|
+
}
|
|
197
|
+
else if (this.rankCompare(item.candidate, top[top.length - 1].candidate) < 0) {
|
|
198
|
+
top[top.length - 1] = item;
|
|
199
|
+
top.sort((a, b) => this.rankCompare(a.candidate, b.candidate));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return top;
|
|
203
|
+
}
|
|
204
|
+
onDay(createDate, day) {
|
|
205
|
+
if (!createDate)
|
|
206
|
+
return true;
|
|
207
|
+
return createDate.slice(0, 10) === day;
|
|
208
|
+
}
|
|
209
|
+
normalize(value) {
|
|
210
|
+
return value.toLowerCase().normalize('NFKC');
|
|
211
|
+
}
|
|
212
|
+
key(value) {
|
|
213
|
+
return value.trim().normalize('NFKC').toLocaleLowerCase();
|
|
214
|
+
}
|
|
215
|
+
bound(value, fallback, min, max) {
|
|
216
|
+
const resolved = value ?? fallback;
|
|
217
|
+
if (!Number.isInteger(resolved) || resolved < min || resolved > max)
|
|
218
|
+
return fallback;
|
|
219
|
+
return resolved;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exports.TopicPipeline = TopicPipeline;
|
|
223
|
+
function seedText(key) { return key; }
|
|
224
|
+
//# sourceMappingURL=TopicPipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicPipeline.js","sourceRoot":"","sources":["../../src/topic/TopicPipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TopicCache } from './TopicCache';
|
|
2
|
+
import type { TopicClient, TopicContentType, TopicDiscoveryOptions, TopicSpace } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a user topic into a bounded, cached set of related Pixiv tags.
|
|
5
|
+
*
|
|
6
|
+
* Discovery is intentionally one-hop around the ORIGINAL seed only (depth = 1):
|
|
7
|
+
* related tags never become new seeds, so the topic cannot drift outward. All
|
|
8
|
+
* network use is bounded and serial. Refresh failures degrade gracefully to a
|
|
9
|
+
* stale cache, then to the seed tag alone.
|
|
10
|
+
*/
|
|
11
|
+
export declare class TopicResolver {
|
|
12
|
+
private readonly client;
|
|
13
|
+
private readonly cache;
|
|
14
|
+
private readonly requestDelayMs;
|
|
15
|
+
private readonly scorer;
|
|
16
|
+
constructor(client: TopicClient, cache: TopicCache, requestDelayMs?: number);
|
|
17
|
+
/** Resolve tags for a topic+type, using cache when fresh. Never throws for
|
|
18
|
+
* discovery errors: it always returns at least the seed tag. */
|
|
19
|
+
resolve(topic: string, contentType: TopicContentType, options?: TopicDiscoveryOptions): Promise<{
|
|
20
|
+
space: TopicSpace;
|
|
21
|
+
fromCache: boolean;
|
|
22
|
+
degraded: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
private discover;
|
|
25
|
+
/** Serial, bounded fetches: autocomplete → topic sample → small background. */
|
|
26
|
+
private gatherSamples;
|
|
27
|
+
private seedOnly;
|
|
28
|
+
private ageDays;
|
|
29
|
+
private bound;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=TopicResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicResolver.d.ts","sourceRoot":"","sources":["../../src/topic/TopicResolver.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAEV,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,UAAU,EAEX,MAAM,SAAS,CAAC;AAiBjB;;;;;;;GAOG;AACH,qBAAa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,cAAc;IALjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAG5B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EACjB,cAAc,SAAM;IAGvC;qEACiE;IAC3D,OAAO,CACX,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,gBAAgB,EAC7B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;YAyC1D,QAAQ;IA+CtB,+EAA+E;YACjE,aAAa;IAsB3B,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,KAAK;CAOd"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TopicResolver = void 0;
|
|
4
|
+
const promises_1 = require("node:timers/promises");
|
|
5
|
+
const logger_1 = require("../logger");
|
|
6
|
+
const TopicTagScorer_1 = require("./TopicTagScorer");
|
|
7
|
+
/** Platform-wide tags never worth searching as topic keys or scoring metadata. */
|
|
8
|
+
const STOP_TAGS = new Set([
|
|
9
|
+
'r-18', 'r-18g', 'r-15', 'original', 'オリジナル', '創作', '女の子', '男の子',
|
|
10
|
+
'イラスト', '漫画', 'manga', 'illustration', 'artwork', '落書き', 'らくがき',
|
|
11
|
+
'1000users入り', '5000users入り', '10000users入り', '500users入り', '100users入り',
|
|
12
|
+
'pixiv', 'commission', 'skeb', '依頼絵', '仕事絵', 'aiイラスト',
|
|
13
|
+
]);
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
maxTags: 12,
|
|
16
|
+
sampleWorks: 100,
|
|
17
|
+
cacheDays: 7,
|
|
18
|
+
minScore: 0.22,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a user topic into a bounded, cached set of related Pixiv tags.
|
|
22
|
+
*
|
|
23
|
+
* Discovery is intentionally one-hop around the ORIGINAL seed only (depth = 1):
|
|
24
|
+
* related tags never become new seeds, so the topic cannot drift outward. All
|
|
25
|
+
* network use is bounded and serial. Refresh failures degrade gracefully to a
|
|
26
|
+
* stale cache, then to the seed tag alone.
|
|
27
|
+
*/
|
|
28
|
+
class TopicResolver {
|
|
29
|
+
client;
|
|
30
|
+
cache;
|
|
31
|
+
requestDelayMs;
|
|
32
|
+
scorer = new TopicTagScorer_1.TopicTagScorer();
|
|
33
|
+
constructor(client, cache, requestDelayMs = 500) {
|
|
34
|
+
this.client = client;
|
|
35
|
+
this.cache = cache;
|
|
36
|
+
this.requestDelayMs = requestDelayMs;
|
|
37
|
+
}
|
|
38
|
+
/** Resolve tags for a topic+type, using cache when fresh. Never throws for
|
|
39
|
+
* discovery errors: it always returns at least the seed tag. */
|
|
40
|
+
async resolve(topic, contentType, options = {}) {
|
|
41
|
+
const seed = topic.trim();
|
|
42
|
+
if (!seed)
|
|
43
|
+
throw new Error('Topic must not be empty');
|
|
44
|
+
const maxTags = this.bound(options.maxTags, DEFAULTS.maxTags, 1, 40, 'maxTags');
|
|
45
|
+
const sampleWorks = this.bound(options.sampleWorks, DEFAULTS.sampleWorks, 10, 200, 'sampleWorks');
|
|
46
|
+
const cacheDays = this.bound(options.cacheDays, DEFAULTS.cacheDays, 1, 90, 'cacheDays');
|
|
47
|
+
const minScore = options.minScore ?? DEFAULTS.minScore;
|
|
48
|
+
if (!options.refresh) {
|
|
49
|
+
const fresh = await this.cache.loadFresh(seed, contentType);
|
|
50
|
+
if (fresh) {
|
|
51
|
+
logger_1.logger.info(`[TopicResolver] topic=${seed} type=${contentType} cache hit age=${this.ageDays(fresh)}d tags=${fresh.tags.length}`);
|
|
52
|
+
return { space: fresh, fromCache: true, degraded: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
logger_1.logger.info(`[TopicResolver] refreshing topic=${seed} type=${contentType}`);
|
|
56
|
+
try {
|
|
57
|
+
const space = await this.discover(seed, contentType, { maxTags, sampleWorks, cacheDays, minScore });
|
|
58
|
+
await this.cache.save(space);
|
|
59
|
+
logger_1.logger.info(`[TopicResolver] topic=${seed} type=${contentType} resolvedTags=${space.tags.length} sampled=${space.sampledWorks}`);
|
|
60
|
+
for (const tag of space.tags.slice(0, maxTags)) {
|
|
61
|
+
logger_1.logger.debug(`[TopicResolver] tag=${tag.name} score=${tag.score} occ=${tag.occurrences} spec=${tag.specificity}`);
|
|
62
|
+
}
|
|
63
|
+
return { space, fromCache: false, degraded: false };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const stale = await this.cache.loadAny(seed, contentType);
|
|
67
|
+
if (stale) {
|
|
68
|
+
logger_1.logger.warn(`[TopicResolver] refresh failed, using stale cache topic=${seed}`, {
|
|
69
|
+
error: error instanceof Error ? error.message : String(error),
|
|
70
|
+
});
|
|
71
|
+
return { space: stale, fromCache: true, degraded: true };
|
|
72
|
+
}
|
|
73
|
+
logger_1.logger.warn(`[TopicResolver] refresh failed with no cache, falling back to seed topic=${seed}`, {
|
|
74
|
+
error: error instanceof Error ? error.message : String(error),
|
|
75
|
+
});
|
|
76
|
+
return { space: this.seedOnly(seed, contentType, cacheDays), fromCache: false, degraded: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async discover(seed, contentType, cfg) {
|
|
80
|
+
const [suggested, topicWorks, backgroundWorks] = await this.gatherSamples(seed, contentType, cfg.sampleWorks);
|
|
81
|
+
const suggestedNames = new Set(suggested.map((t) => this.scorer.key(t.name)).filter(Boolean));
|
|
82
|
+
const related = this.scorer.score({
|
|
83
|
+
seed,
|
|
84
|
+
topicWorks,
|
|
85
|
+
backgroundWorks,
|
|
86
|
+
suggestedNames,
|
|
87
|
+
});
|
|
88
|
+
// Seed tag always leads the space with score 1.0; keep related tags above
|
|
89
|
+
// the relevance floor and cap the total.
|
|
90
|
+
const seedTag = {
|
|
91
|
+
name: seed,
|
|
92
|
+
translatedName: suggested.find((t) => this.scorer.key(t.name) === this.scorer.key(seed))?.translated_name,
|
|
93
|
+
score: 1,
|
|
94
|
+
occurrences: topicWorks.length,
|
|
95
|
+
coverage: 1,
|
|
96
|
+
specificity: 1,
|
|
97
|
+
suggested: suggestedNames.has(this.scorer.key(seed)),
|
|
98
|
+
seed: true,
|
|
99
|
+
};
|
|
100
|
+
const accepted = related
|
|
101
|
+
.filter((tag) => tag.score >= cfg.minScore)
|
|
102
|
+
.filter((tag) => !STOP_TAGS.has(this.scorer.key(tag.name)))
|
|
103
|
+
.slice(0, cfg.maxTags - 1);
|
|
104
|
+
const tags = [seedTag, ...accepted];
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
return {
|
|
107
|
+
version: 1,
|
|
108
|
+
topic: seed,
|
|
109
|
+
contentType,
|
|
110
|
+
createdAt: new Date(now).toISOString(),
|
|
111
|
+
expiresAt: new Date(now + cfg.cacheDays * 24 * 60 * 60_000).toISOString(),
|
|
112
|
+
sampleSize: cfg.sampleWorks,
|
|
113
|
+
sampledWorks: topicWorks.length,
|
|
114
|
+
tags,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Serial, bounded fetches: autocomplete → topic sample → small background. */
|
|
118
|
+
async gatherSamples(seed, contentType, sampleWorks) {
|
|
119
|
+
const suggested = await this.client.getTagAutocomplete(seed).catch(() => []);
|
|
120
|
+
if (this.requestDelayMs > 0)
|
|
121
|
+
await (0, promises_1.setTimeout)(this.requestDelayMs);
|
|
122
|
+
const topicWorks = contentType === 'illustration'
|
|
123
|
+
? await this.client.searchIllustrationsForTags(seed, sampleWorks)
|
|
124
|
+
: await this.client.searchNovelsForTags(seed, sampleWorks);
|
|
125
|
+
// A small, cheap background sample (a common platform tag) estimates how
|
|
126
|
+
// generic a co-occurring tag is. Bounded to keep requests/memory low.
|
|
127
|
+
if (this.requestDelayMs > 0)
|
|
128
|
+
await (0, promises_1.setTimeout)(this.requestDelayMs);
|
|
129
|
+
const backgroundWorks = contentType === 'illustration'
|
|
130
|
+
? await this.client.searchIllustrationsForTags('イラスト', 40).catch(() => [])
|
|
131
|
+
: await this.client.searchNovelsForTags('小説', 40).catch(() => []);
|
|
132
|
+
return [suggested, topicWorks, backgroundWorks];
|
|
133
|
+
}
|
|
134
|
+
seedOnly(seed, contentType, cacheDays) {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
return {
|
|
137
|
+
version: 1,
|
|
138
|
+
topic: seed,
|
|
139
|
+
contentType,
|
|
140
|
+
createdAt: new Date(now).toISOString(),
|
|
141
|
+
expiresAt: new Date(now + cacheDays * 24 * 60 * 60_000).toISOString(),
|
|
142
|
+
sampleSize: 0,
|
|
143
|
+
sampledWorks: 0,
|
|
144
|
+
tags: [{ name: seed, score: 1, occurrences: 0, coverage: 1, specificity: 1, suggested: false, seed: true }],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
ageDays(space) {
|
|
148
|
+
const created = Date.parse(space.createdAt);
|
|
149
|
+
if (!Number.isFinite(created))
|
|
150
|
+
return 0;
|
|
151
|
+
return Math.max(0, Math.round((Date.now() - created) / 86_400_000));
|
|
152
|
+
}
|
|
153
|
+
bound(value, fallback, min, max, name) {
|
|
154
|
+
const resolved = value ?? fallback;
|
|
155
|
+
if (!Number.isInteger(resolved) || resolved < min || resolved > max) {
|
|
156
|
+
throw new Error(`topicDiscovery.${name} must be an integer between ${min} and ${max}`);
|
|
157
|
+
}
|
|
158
|
+
return resolved;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.TopicResolver = TopicResolver;
|
|
162
|
+
//# sourceMappingURL=TopicResolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicResolver.js","sourceRoot":"","sources":["../../src/topic/TopicResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedTag, WorkLike } from './types';
|
|
2
|
+
export interface ScorerInput {
|
|
3
|
+
seed: string;
|
|
4
|
+
topicWorks: WorkLike[];
|
|
5
|
+
backgroundWorks: WorkLike[];
|
|
6
|
+
suggestedNames: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare class TopicTagScorer {
|
|
9
|
+
/** Normalize for matching/keys: trim, NFKC, lower-case. */
|
|
10
|
+
key(value: string): string;
|
|
11
|
+
score(input: ScorerInput): ResolvedTag[];
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=TopicTagScorer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicTagScorer.d.ts","sourceRoot":"","sources":["../../src/topic/TopicTagScorer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAoCrD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvB,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED,qBAAa,cAAc;IACzB,2DAA2D;IAC3D,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAI1B,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,EAAE;CA+EzC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TopicTagScorer = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Lightweight, explainable tag relatedness. No ML — pure co-occurrence stats
|
|
6
|
+
* over the small, bounded samples the resolver fetches.
|
|
7
|
+
*
|
|
8
|
+
* score = coverage * specificity * suggestionWeight
|
|
9
|
+
*
|
|
10
|
+
* - coverage: fraction of the seed-topic sample that carries the tag.
|
|
11
|
+
* - specificity: how much more often the tag appears with the topic than in
|
|
12
|
+
* the background. High-frequency but topic-agnostic tags
|
|
13
|
+
* (R-18, オリジナル, 女の子…) get pushed down automatically.
|
|
14
|
+
* - suggestionWeight: a modest boost when Pixiv autocomplete also returns it.
|
|
15
|
+
*
|
|
16
|
+
* The background document frequency is estimated from a cheap background
|
|
17
|
+
* sample (a recent, unrelated search) when available, falling back to a small
|
|
18
|
+
* curated set of platform-wide tags. We deliberately keep that list tiny — the
|
|
19
|
+
* specificity score does the real denoising.
|
|
20
|
+
*/
|
|
21
|
+
/** Platform-level tags that are almost never topic-specific. */
|
|
22
|
+
const GENERIC_TAG_PENALTY = new Set([
|
|
23
|
+
'r-18', 'r-18g', 'r-15', 'original', 'オリジナル', '創作', '女の子', '男の子',
|
|
24
|
+
'イラスト', '漫画', 'manga', 'illustration', 'artwork', '落書き', 'らくがき',
|
|
25
|
+
'1000users入り', '5000users入り', '10000users入り', '500users入り', '100users入り',
|
|
26
|
+
'pixiv', 'commission', 'skeb', '依頼絵', '仕事絵',
|
|
27
|
+
]);
|
|
28
|
+
class TopicTagScorer {
|
|
29
|
+
/** Normalize for matching/keys: trim, NFKC, lower-case. */
|
|
30
|
+
key(value) {
|
|
31
|
+
return value.trim().normalize('NFKC').toLocaleLowerCase();
|
|
32
|
+
}
|
|
33
|
+
score(input) {
|
|
34
|
+
const seedKey = this.key(input.seed);
|
|
35
|
+
const topicSize = Math.max(input.topicWorks.length, 1);
|
|
36
|
+
const backgroundSize = Math.max(input.backgroundWorks.length, 1);
|
|
37
|
+
const stats = new Map();
|
|
38
|
+
const accumulate = (works, bucket) => {
|
|
39
|
+
for (const work of works) {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const tag of work.tags ?? []) {
|
|
42
|
+
const name = tag.name?.trim();
|
|
43
|
+
const k = this.key(name ?? '');
|
|
44
|
+
if (!k || seen.has(k))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(k);
|
|
47
|
+
let stat = stats.get(k);
|
|
48
|
+
if (!stat) {
|
|
49
|
+
stat = { name: name, translatedName: tag.translated_name?.trim() || undefined, topicDocs: 0, backgroundDocs: 0, suggested: false };
|
|
50
|
+
stats.set(k, stat);
|
|
51
|
+
}
|
|
52
|
+
else if (!stat.translatedName && tag.translated_name?.trim()) {
|
|
53
|
+
stat.translatedName = tag.translated_name.trim();
|
|
54
|
+
}
|
|
55
|
+
stat[bucket] += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
accumulate(input.topicWorks, 'topicDocs');
|
|
60
|
+
accumulate(input.backgroundWorks, 'backgroundDocs');
|
|
61
|
+
for (const name of input.suggestedNames) {
|
|
62
|
+
const k = this.key(name);
|
|
63
|
+
const stat = stats.get(k);
|
|
64
|
+
if (stat)
|
|
65
|
+
stat.suggested = true;
|
|
66
|
+
}
|
|
67
|
+
const resolved = [];
|
|
68
|
+
for (const [k, stat] of stats) {
|
|
69
|
+
if (k === seedKey)
|
|
70
|
+
continue; // seed is added separately with score 1
|
|
71
|
+
const coverage = stat.topicDocs / topicSize;
|
|
72
|
+
if (stat.topicDocs <= 0)
|
|
73
|
+
continue;
|
|
74
|
+
// Background frequency of the tag in the unrelated sample. Tags that are
|
|
75
|
+
// common everywhere (R-18, オリジナル…) have high backgroundFreq; tags
|
|
76
|
+
// that only travel with the topic have ~0.
|
|
77
|
+
const backgroundFreq = stat.backgroundDocs / backgroundSize;
|
|
78
|
+
// PMI-style lift: how much more the tag appears with the topic than at
|
|
79
|
+
// large. Positive (and growing for background-absent tags), near/below 0
|
|
80
|
+
// for generic tags. Additive smoothing prevents divide-by-zero blowups.
|
|
81
|
+
const lift = Math.log((coverage + 0.03) / (backgroundFreq + 0.03));
|
|
82
|
+
// Map lift to a 0..1+ specificity via a logistic-ish squish; a lift of ~0
|
|
83
|
+
// (equally common in background) maps to ~0.5 and strongly generic tags
|
|
84
|
+
// fall below it.
|
|
85
|
+
const specificity = Math.max(0, Math.min(1.2, lift / 3 + 0.5));
|
|
86
|
+
// Final score rewards both co-occurrence (recall) and specificity
|
|
87
|
+
// (precision), so rare-but-exclusive tags can still surface while
|
|
88
|
+
// ubiquitous tags are suppressed.
|
|
89
|
+
const recall = Math.sqrt(coverage); // dampen so mid-coverage can win
|
|
90
|
+
const suggestionWeight = stat.suggested ? 1.1 : 1.0;
|
|
91
|
+
const genericPenalty = GENERIC_TAG_PENALTY.has(k) ? 0.4 : 1.0;
|
|
92
|
+
const raw = recall * specificity * suggestionWeight * genericPenalty;
|
|
93
|
+
resolved.push({
|
|
94
|
+
name: stat.name,
|
|
95
|
+
translatedName: stat.translatedName,
|
|
96
|
+
score: Number(raw.toFixed(4)),
|
|
97
|
+
occurrences: stat.topicDocs,
|
|
98
|
+
coverage: Number(coverage.toFixed(4)),
|
|
99
|
+
specificity: Number(specificity.toFixed(4)),
|
|
100
|
+
suggested: stat.suggested,
|
|
101
|
+
seed: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
resolved.sort((a, b) => b.score - a.score || b.occurrences - a.occurrences || a.name.localeCompare(b.name));
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.TopicTagScorer = TopicTagScorer;
|
|
109
|
+
//# sourceMappingURL=TopicTagScorer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicTagScorer.js","sourceRoot":"","sources":["../../src/topic/TopicTagScorer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IPixivClient } from '../interfaces/IPixivClient';
|
|
2
|
+
import { TopicPipeline } from './TopicPipeline';
|
|
3
|
+
export type TopicPipelineFactory = () => TopicPipeline;
|
|
4
|
+
/**
|
|
5
|
+
* Builds the topic pipeline with a persistent cache beside the SQLite database
|
|
6
|
+
* (which lives on the data volume in Docker, so it survives container rebuilds).
|
|
7
|
+
* One instance is shared across targets within a download run.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createTopicPipeline(client: IPixivClient, databasePath: string | undefined, requestDelayMs?: number): TopicPipeline;
|
|
10
|
+
/** Lazily builds one shared pipeline per download run (cache + resolver). */
|
|
11
|
+
export declare function createTopicPipelineFactory(client: IPixivClient, databasePath: string | undefined, requestDelayMs?: number): TopicPipelineFactory;
|
|
12
|
+
//# sourceMappingURL=createTopicPipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTopicPipeline.d.ts","sourceRoot":"","sources":["../../src/topic/createTopicPipeline.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,MAAM,oBAAoB,GAAG,MAAM,aAAa,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE,cAAc,SAAM,GAAG,aAAa,CAO/H;AAED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE,cAAc,SAAM,GAAG,oBAAoB,CAM7I"}
|