@kaikybrofc/omnizap-system 2.1.8
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/.env.example +534 -0
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/RELEASE-v2.1.2.md +83 -0
- package/app/config/adminIdentity.js +87 -0
- package/app/config/baileysConfig.js +693 -0
- package/app/config/groupUtils.js +388 -0
- package/app/connection/socketController.js +992 -0
- package/app/controllers/messageController.js +354 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
- package/app/modules/adminModule/groupEventHandlers.js +355 -0
- package/app/modules/aiModule/catCommand.js +1006 -0
- package/app/modules/broadcastModule/noticeCommand.js +416 -0
- package/app/modules/gameModule/diceCommand.js +67 -0
- package/app/modules/menuModule/common.js +311 -0
- package/app/modules/menuModule/menus.js +59 -0
- package/app/modules/playModule/playCommand.js +1615 -0
- package/app/modules/quoteModule/quoteCommand.js +851 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/globalRankingCommand.js +65 -0
- package/app/modules/statsModule/noMessageCommand.js +288 -0
- package/app/modules/statsModule/rankingCommand.js +60 -0
- package/app/modules/statsModule/rankingCommon.js +889 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/convertToWebp.js +390 -0
- package/app/modules/stickerModule/stickerCommand.js +454 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
- package/app/modules/stickerModule/stickerTextCommand.js +657 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
- package/app/modules/stickerPackModule/stickerPackService.js +788 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
- package/app/modules/systemMetricsModule/pingCommand.js +421 -0
- package/app/modules/tiktokModule/tiktokCommand.js +798 -0
- package/app/modules/userModule/userCommand.js +1217 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
- package/app/observability/metrics.js +734 -0
- package/app/services/captchaService.js +492 -0
- package/app/services/dbWriteQueue.js +572 -0
- package/app/services/groupMetadataService.js +279 -0
- package/app/services/lidMapService.js +663 -0
- package/app/services/messagePersistenceService.js +56 -0
- package/app/services/newsBroadcastService.js +351 -0
- package/app/services/pokeApiService.js +398 -0
- package/app/services/queueUtils.js +57 -0
- package/app/services/socketState.js +7 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +36 -0
- package/app/utils/antiLink/antiLinkModule.js +804 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/logger/loggerModule.js +262 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
- package/database/index.js +2052 -0
- package/database/init.js +516 -0
- package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
- package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
- package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
- package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
- package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
- package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
- package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
- package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
- package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
- package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
- package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
- package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
- package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
- package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
- package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
- package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
- package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
- package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
- package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
- package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
- package/docker-compose.yml +103 -0
- package/ecosystem.prod.config.cjs +35 -0
- package/eslint.config.js +61 -0
- package/index.js +437 -0
- package/ml/clip_classifier/Dockerfile +16 -0
- package/ml/clip_classifier/README.md +120 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-exporter.cnf +5 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +32 -0
- package/observability/promtail-config.yml +84 -0
- package/package.json +109 -0
- package/public/api-docs/index.html +144 -0
- package/public/css/github-project-panel.css +297 -0
- package/public/css/stickers-admin.css +1272 -0
- package/public/css/styles.css +671 -0
- package/public/index.html +1311 -0
- package/public/js/apps/apiDocsApp.js +310 -0
- package/public/js/apps/createPackApp.js +2069 -0
- package/public/js/apps/homeApp.js +396 -0
- package/public/js/apps/stickersAdminApp.js +1744 -0
- package/public/js/apps/stickersApp.js +4830 -0
- package/public/js/catalog.js +1019 -0
- package/public/js/github-panel/components/CommitList.js +34 -0
- package/public/js/github-panel/components/ErrorState.js +16 -0
- package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
- package/public/js/github-panel/components/ReleaseList.js +38 -0
- package/public/js/github-panel/components/SkeletonPanel.js +22 -0
- package/public/js/github-panel/components/StatCard.js +15 -0
- package/public/js/github-panel/index.js +15 -0
- package/public/js/github-panel/useGithubRepoData.js +154 -0
- package/public/js/github-panel/vendor/react.js +11 -0
- package/public/js/runtime/react-runtime.js +19 -0
- package/public/licenca/index.html +106 -0
- package/public/stickers/admin/index.html +23 -0
- package/public/stickers/create/index.html +47 -0
- package/public/stickers/index.html +48 -0
- package/public/termos-de-uso/index.html +125 -0
- package/scripts/cache-bust.mjs +107 -0
- package/scripts/deploy.sh +458 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/release.sh +129 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
2
|
+
|
|
3
|
+
const parseEnvBool = (value, fallback) => {
|
|
4
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
5
|
+
const normalized = String(value).trim().toLowerCase();
|
|
6
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
7
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
8
|
+
return fallback;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const clampNumber = (value, min, max) => Math.max(min, Math.min(max, Number(value)));
|
|
12
|
+
|
|
13
|
+
const RECLASSIFICATION_ENABLED = parseEnvBool(process.env.STICKER_SEMANTIC_RECLASSIFICATION_ENABLED, true);
|
|
14
|
+
const RECLASSIFICATION_BATCH_SIZE = Math.max(
|
|
15
|
+
50,
|
|
16
|
+
Math.min(2000, Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_BATCH_SIZE) || 400),
|
|
17
|
+
);
|
|
18
|
+
const RECLASSIFICATION_MAX_PER_CYCLE = Math.max(
|
|
19
|
+
100,
|
|
20
|
+
Math.min(20_000, Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_MAX_PER_CYCLE) || 2000),
|
|
21
|
+
);
|
|
22
|
+
const RECLASSIFICATION_ENTROPY_THRESHOLD = Number.isFinite(Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_ENTROPY_THRESHOLD))
|
|
23
|
+
? Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_ENTROPY_THRESHOLD)
|
|
24
|
+
: 0.8;
|
|
25
|
+
const RECLASSIFICATION_AFFINITY_THRESHOLD = Number.isFinite(Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_AFFINITY_THRESHOLD))
|
|
26
|
+
? Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_AFFINITY_THRESHOLD)
|
|
27
|
+
: 0.3;
|
|
28
|
+
|
|
29
|
+
const STOPWORDS = [
|
|
30
|
+
'image',
|
|
31
|
+
'sticker',
|
|
32
|
+
'wallpaper',
|
|
33
|
+
'social_media',
|
|
34
|
+
'internet',
|
|
35
|
+
'picture',
|
|
36
|
+
];
|
|
37
|
+
const GENERIC_TERMS = [
|
|
38
|
+
'cool',
|
|
39
|
+
'nice',
|
|
40
|
+
'funny',
|
|
41
|
+
'random',
|
|
42
|
+
'art',
|
|
43
|
+
];
|
|
44
|
+
const SEMANTIC_GROUPS = ['anime', 'meme', 'kawaii', 'horror', 'reaction'];
|
|
45
|
+
const OPPOSITE_THEME_PAIRS = [
|
|
46
|
+
['kawaii', 'horror'],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const DICTIONARY_MAP = {
|
|
50
|
+
'cute anime girl': 'kawaii_anime_girl',
|
|
51
|
+
'funny reaction': 'exaggerated_reaction',
|
|
52
|
+
'meme image': 'meme_reaction',
|
|
53
|
+
'anime emotion': 'anime_expression',
|
|
54
|
+
'chat expression': 'chat_reaction',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const toSnakeCase = (value) =>
|
|
58
|
+
String(value || '')
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.normalize('NFD')
|
|
62
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
63
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
64
|
+
.replace(/^_+|_+$/g, '')
|
|
65
|
+
.replace(/_+/g, '_');
|
|
66
|
+
|
|
67
|
+
const STOPWORD_PHRASES = new Set(STOPWORDS.map((value) => toSnakeCase(value)).filter(Boolean));
|
|
68
|
+
const STOPWORD_WORDS = new Set(
|
|
69
|
+
STOPWORDS
|
|
70
|
+
.map((value) => toSnakeCase(value))
|
|
71
|
+
.flatMap((value) => value.split('_'))
|
|
72
|
+
.filter((value) => value.length >= 3),
|
|
73
|
+
);
|
|
74
|
+
const GENERIC_TERM_SET = new Set(GENERIC_TERMS.map((value) => toSnakeCase(value)).filter(Boolean));
|
|
75
|
+
const GENERIC_TERM_WORDS = new Set(
|
|
76
|
+
GENERIC_TERMS
|
|
77
|
+
.map((value) => toSnakeCase(value))
|
|
78
|
+
.flatMap((value) => value.split('_'))
|
|
79
|
+
.filter((value) => value.length >= 3),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const normalizeTokenValue = (value) => {
|
|
83
|
+
const slug = toSnakeCase(value);
|
|
84
|
+
if (!slug || slug.length < 3) return '';
|
|
85
|
+
if (STOPWORD_PHRASES.has(slug) || GENERIC_TERM_SET.has(slug)) return '';
|
|
86
|
+
|
|
87
|
+
const words = slug
|
|
88
|
+
.split('_')
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.filter((word) => word.length >= 3)
|
|
91
|
+
.filter((word) => !STOPWORD_WORDS.has(word))
|
|
92
|
+
.filter((word) => !GENERIC_TERM_WORDS.has(word));
|
|
93
|
+
if (!words.length) return '';
|
|
94
|
+
|
|
95
|
+
const token = words.join('_');
|
|
96
|
+
if (!token || token.length < 3) return '';
|
|
97
|
+
if (STOPWORD_PHRASES.has(token) || GENERIC_TERM_SET.has(token)) return '';
|
|
98
|
+
return token;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const DICTIONARY_ENTRIES = Object.entries(DICTIONARY_MAP)
|
|
102
|
+
.map(([rawSource, rawTarget]) => ({
|
|
103
|
+
source: normalizeTokenValue(rawSource),
|
|
104
|
+
target: normalizeTokenValue(rawTarget),
|
|
105
|
+
}))
|
|
106
|
+
.filter((entry) => entry.source && entry.target)
|
|
107
|
+
.sort((left, right) => right.source.length - left.source.length);
|
|
108
|
+
|
|
109
|
+
const extractTopLabelTokens = (topLabels) => {
|
|
110
|
+
if (!Array.isArray(topLabels)) return [];
|
|
111
|
+
return topLabels
|
|
112
|
+
.map((entry) => {
|
|
113
|
+
if (!entry) return '';
|
|
114
|
+
if (typeof entry === 'string') return entry;
|
|
115
|
+
if (typeof entry?.label === 'string') return entry.label;
|
|
116
|
+
return '';
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const registerToken = (tokenMap, token) => {
|
|
122
|
+
if (!token) return;
|
|
123
|
+
const current = tokenMap.get(token) || 0;
|
|
124
|
+
tokenMap.set(token, current + 1);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const mapEntriesToSortedArray = (tokenMap) =>
|
|
128
|
+
Array.from(tokenMap.entries())
|
|
129
|
+
.map(([token, weight]) => ({
|
|
130
|
+
token,
|
|
131
|
+
weight: Number(weight || 0),
|
|
132
|
+
}))
|
|
133
|
+
.filter((entry) => entry.token && entry.weight > 0)
|
|
134
|
+
.sort((left, right) => {
|
|
135
|
+
if (right.weight !== left.weight) return right.weight - left.weight;
|
|
136
|
+
return left.token.localeCompare(right.token);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export const normalizeTokens = (classification = {}) => {
|
|
140
|
+
const tokenMap = new Map();
|
|
141
|
+
const pushToken = (rawValue) => {
|
|
142
|
+
const normalized = normalizeTokenValue(rawValue);
|
|
143
|
+
if (!normalized) return;
|
|
144
|
+
registerToken(tokenMap, normalized);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const label of extractTopLabelTokens(classification?.top_labels)) {
|
|
148
|
+
pushToken(label);
|
|
149
|
+
}
|
|
150
|
+
for (const value of Array.isArray(classification?.llm_subtags) ? classification.llm_subtags : []) {
|
|
151
|
+
pushToken(value);
|
|
152
|
+
}
|
|
153
|
+
for (const value of Array.isArray(classification?.llm_style_traits) ? classification.llm_style_traits : []) {
|
|
154
|
+
pushToken(value);
|
|
155
|
+
}
|
|
156
|
+
for (const value of Array.isArray(classification?.llm_emotions) ? classification.llm_emotions : []) {
|
|
157
|
+
pushToken(value);
|
|
158
|
+
}
|
|
159
|
+
for (const value of Array.isArray(classification?.llm_pack_suggestions) ? classification.llm_pack_suggestions : []) {
|
|
160
|
+
pushToken(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return mapEntriesToSortedArray(tokenMap);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const resolveDictionaryToken = (token) => {
|
|
167
|
+
const normalizedToken = normalizeTokenValue(token);
|
|
168
|
+
if (!normalizedToken) return '';
|
|
169
|
+
|
|
170
|
+
for (const entry of DICTIONARY_ENTRIES) {
|
|
171
|
+
if (normalizedToken === entry.source) return entry.target;
|
|
172
|
+
if (normalizedToken.includes(entry.source)) return entry.target;
|
|
173
|
+
if (entry.source.includes(normalizedToken)) return entry.target;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return normalizedToken;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const applyDictionaryMapping = (tokens = []) => {
|
|
180
|
+
const mapped = new Map();
|
|
181
|
+
|
|
182
|
+
for (const entry of Array.isArray(tokens) ? tokens : []) {
|
|
183
|
+
const mappedToken = resolveDictionaryToken(entry?.token || entry);
|
|
184
|
+
if (!mappedToken) continue;
|
|
185
|
+
const weight = Math.max(1, Number(entry?.weight || 1));
|
|
186
|
+
const current = mapped.get(mappedToken) || 0;
|
|
187
|
+
mapped.set(mappedToken, current + weight);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return mapEntriesToSortedArray(mapped);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const resolveSemanticGroup = (token) => {
|
|
194
|
+
const normalized = normalizeTokenValue(token);
|
|
195
|
+
if (!normalized) return 'other';
|
|
196
|
+
|
|
197
|
+
for (const group of SEMANTIC_GROUPS) {
|
|
198
|
+
if (normalized === group || normalized.startsWith(`${group}_`)) {
|
|
199
|
+
return group;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (/_reaction$/.test(normalized) || /_expression$/.test(normalized)) return 'reaction';
|
|
204
|
+
if (/_anime($|_)/.test(normalized)) return 'anime';
|
|
205
|
+
if (/_meme($|_)/.test(normalized)) return 'meme';
|
|
206
|
+
if (/_kawaii($|_)/.test(normalized)) return 'kawaii';
|
|
207
|
+
if (/_horror($|_)/.test(normalized)) return 'horror';
|
|
208
|
+
|
|
209
|
+
return 'other';
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const detectDominantTheme = (tokens = []) => {
|
|
213
|
+
const themeWeights = new Map();
|
|
214
|
+
let totalWeight = 0;
|
|
215
|
+
|
|
216
|
+
for (const entry of Array.isArray(tokens) ? tokens : []) {
|
|
217
|
+
const token = normalizeTokenValue(entry?.token || entry);
|
|
218
|
+
if (!token) continue;
|
|
219
|
+
const weight = Math.max(1, Number(entry?.weight || 1));
|
|
220
|
+
const group = resolveSemanticGroup(token);
|
|
221
|
+
themeWeights.set(group, (themeWeights.get(group) || 0) + weight);
|
|
222
|
+
totalWeight += weight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const sortedThemes = Array.from(themeWeights.entries())
|
|
226
|
+
.map(([theme, weight]) => ({ theme, weight: Number(weight || 0) }))
|
|
227
|
+
.sort((left, right) => {
|
|
228
|
+
if (right.weight !== left.weight) return right.weight - left.weight;
|
|
229
|
+
return left.theme.localeCompare(right.theme);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const dominantTheme = sortedThemes[0]?.theme || '';
|
|
233
|
+
const dominantWeight = Number(sortedThemes[0]?.weight || 0);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
dominant_theme: dominantTheme,
|
|
237
|
+
dominant_weight: dominantWeight,
|
|
238
|
+
total_weight: Number(totalWeight || 0),
|
|
239
|
+
ranked_themes: sortedThemes,
|
|
240
|
+
theme_weights_map: themeWeights,
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const calculateCohesion = ({ dominantWeight = 0, totalWeight = 0 } = {}) => {
|
|
245
|
+
const dominant = Math.max(0, Number(dominantWeight || 0));
|
|
246
|
+
const total = Math.max(0, Number(totalWeight || 0));
|
|
247
|
+
if (!total || !dominant) return 0;
|
|
248
|
+
return Number(((dominant / total) * 100).toFixed(6));
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const detectConflict = ({ themeWeights = new Map(), totalWeight = 0 } = {}) => {
|
|
252
|
+
const total = Math.max(0, Number(totalWeight || 0));
|
|
253
|
+
const ranked = Array.from(themeWeights.entries())
|
|
254
|
+
.map(([theme, weight]) => ({ theme, weight: Number(weight || 0) }))
|
|
255
|
+
.filter((entry) => entry.weight > 0)
|
|
256
|
+
.sort((left, right) => {
|
|
257
|
+
if (right.weight !== left.weight) return right.weight - left.weight;
|
|
258
|
+
return left.theme.localeCompare(right.theme);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let ambiguous = 0;
|
|
262
|
+
let penaltyPoints = 0;
|
|
263
|
+
|
|
264
|
+
if (ranked.length > 1 && total > 0) {
|
|
265
|
+
const firstPercent = (ranked[0].weight / total) * 100;
|
|
266
|
+
const secondPercent = (ranked[1].weight / total) * 100;
|
|
267
|
+
if (Math.abs(firstPercent - secondPercent) < 15) {
|
|
268
|
+
ambiguous = 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const weightByTheme = new Map(ranked.map((entry) => [entry.theme, entry.weight]));
|
|
273
|
+
for (const [leftTheme, rightTheme] of OPPOSITE_THEME_PAIRS) {
|
|
274
|
+
const leftWeight = Number(weightByTheme.get(leftTheme) || 0);
|
|
275
|
+
const rightWeight = Number(weightByTheme.get(rightTheme) || 0);
|
|
276
|
+
if (leftWeight > 0 && rightWeight > 0) {
|
|
277
|
+
penaltyPoints += 20;
|
|
278
|
+
ambiguous = 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
ambiguous,
|
|
284
|
+
penalty_points: penaltyPoints,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const normalizeExistingSubtags = (values = []) => {
|
|
289
|
+
const ordered = [];
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
292
|
+
const token = normalizeTokenValue(value);
|
|
293
|
+
if (!token || seen.has(token)) continue;
|
|
294
|
+
seen.add(token);
|
|
295
|
+
ordered.push(token);
|
|
296
|
+
}
|
|
297
|
+
return ordered;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const areListsEqual = (left = [], right = []) => {
|
|
301
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
302
|
+
if (left.length !== right.length) return false;
|
|
303
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
304
|
+
if (left[index] !== right[index]) return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const hasNumericDifference = (left, right, epsilon = 0.000001) => {
|
|
310
|
+
const a = Number(left);
|
|
311
|
+
const b = Number(right);
|
|
312
|
+
if (!Number.isFinite(a) && !Number.isFinite(b)) return false;
|
|
313
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return true;
|
|
314
|
+
return Math.abs(a - b) > epsilon;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
let cachedRepositoryModulePromise = null;
|
|
318
|
+
const resolveRepositoryModule = async () => {
|
|
319
|
+
if (cachedRepositoryModulePromise) return cachedRepositoryModulePromise;
|
|
320
|
+
cachedRepositoryModulePromise = import('./stickerAssetClassificationRepository.js');
|
|
321
|
+
return cachedRepositoryModulePromise;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const reclassify = (classification = {}) => {
|
|
325
|
+
const normalizedTokens = normalizeTokens(classification);
|
|
326
|
+
const mappedTokens = applyDictionaryMapping(normalizedTokens);
|
|
327
|
+
const dominant = detectDominantTheme(mappedTokens);
|
|
328
|
+
|
|
329
|
+
const rawCohesion = calculateCohesion({
|
|
330
|
+
dominantWeight: dominant.dominant_weight,
|
|
331
|
+
totalWeight: dominant.total_weight,
|
|
332
|
+
});
|
|
333
|
+
const conflict = detectConflict({
|
|
334
|
+
themeWeights: dominant.theme_weights_map,
|
|
335
|
+
totalWeight: dominant.total_weight,
|
|
336
|
+
});
|
|
337
|
+
const cohesionScore = Number(clampNumber(rawCohesion - Number(conflict.penalty_points || 0), 0, 100).toFixed(6));
|
|
338
|
+
const dominantTheme = dominant.dominant_theme || 'other';
|
|
339
|
+
|
|
340
|
+
let dominantTokens = mappedTokens.filter((entry) => resolveSemanticGroup(entry.token) === dominantTheme);
|
|
341
|
+
if (!dominantTokens.length) {
|
|
342
|
+
dominantTokens = mappedTokens.slice();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const normalizedSubtags = dominantTokens
|
|
346
|
+
.map((entry) => normalizeTokenValue(entry.token))
|
|
347
|
+
.filter(Boolean);
|
|
348
|
+
|
|
349
|
+
const outputSubtags = Array.from(new Set(normalizedSubtags));
|
|
350
|
+
const updatedAffinityWeight = Number(clampNumber(cohesionScore / 100, 0, 1).toFixed(6));
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
normalized_subtags: outputSubtags,
|
|
354
|
+
dominant_theme: dominantTheme,
|
|
355
|
+
cohesion_score: cohesionScore,
|
|
356
|
+
ambiguous: conflict.ambiguous ? 1 : 0,
|
|
357
|
+
updated_affinity_weight: updatedAffinityWeight,
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export const batchReprocess = async ({
|
|
362
|
+
maxItems = RECLASSIFICATION_MAX_PER_CYCLE,
|
|
363
|
+
batchSize = RECLASSIFICATION_BATCH_SIZE,
|
|
364
|
+
entropyThreshold = RECLASSIFICATION_ENTROPY_THRESHOLD,
|
|
365
|
+
affinityThreshold = RECLASSIFICATION_AFFINITY_THRESHOLD,
|
|
366
|
+
} = {}) => {
|
|
367
|
+
const safeMaxItems = Math.max(0, Math.min(50_000, Number(maxItems) || RECLASSIFICATION_MAX_PER_CYCLE));
|
|
368
|
+
const safeBatchSize = Math.max(1, Math.min(2000, Number(batchSize) || RECLASSIFICATION_BATCH_SIZE));
|
|
369
|
+
|
|
370
|
+
const stats = {
|
|
371
|
+
enabled: RECLASSIFICATION_ENABLED,
|
|
372
|
+
processed: 0,
|
|
373
|
+
updated: 0,
|
|
374
|
+
skipped: 0,
|
|
375
|
+
failed: 0,
|
|
376
|
+
batches: 0,
|
|
377
|
+
last_cursor: null,
|
|
378
|
+
entropy_threshold: Number(entropyThreshold),
|
|
379
|
+
affinity_threshold: Number(affinityThreshold),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
if (!RECLASSIFICATION_ENABLED || safeMaxItems <= 0) {
|
|
383
|
+
return stats;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const repositoryModule = await resolveRepositoryModule();
|
|
387
|
+
const listForReprocess = repositoryModule.listStickerClassificationsForDeterministicReprocess;
|
|
388
|
+
const updateSignals = repositoryModule.updateStickerClassificationDeterministicSignals;
|
|
389
|
+
|
|
390
|
+
let cursorAssetId = '';
|
|
391
|
+
while (stats.processed < safeMaxItems) {
|
|
392
|
+
const remaining = safeMaxItems - stats.processed;
|
|
393
|
+
const pageLimit = Math.max(1, Math.min(safeBatchSize, remaining));
|
|
394
|
+
const rows = await listForReprocess({
|
|
395
|
+
limit: pageLimit,
|
|
396
|
+
cursorAssetId,
|
|
397
|
+
entropyThreshold,
|
|
398
|
+
affinityThreshold,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (!rows.length) break;
|
|
402
|
+
stats.batches += 1;
|
|
403
|
+
|
|
404
|
+
for (const row of rows) {
|
|
405
|
+
const assetId = String(row?.asset_id || '').trim();
|
|
406
|
+
if (!assetId) continue;
|
|
407
|
+
cursorAssetId = assetId;
|
|
408
|
+
stats.last_cursor = assetId;
|
|
409
|
+
stats.processed += 1;
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const output = reclassify(row);
|
|
413
|
+
const currentSubtags = normalizeExistingSubtags(row?.llm_subtags || []);
|
|
414
|
+
const nextSubtags = normalizeExistingSubtags(output.normalized_subtags || []);
|
|
415
|
+
const currentAffinity = row?.affinity_weight;
|
|
416
|
+
const nextAffinity = output.updated_affinity_weight;
|
|
417
|
+
const currentAmbiguous = row?.ambiguous ? 1 : 0;
|
|
418
|
+
const nextAmbiguous = output.ambiguous ? 1 : 0;
|
|
419
|
+
|
|
420
|
+
const shouldUpdate =
|
|
421
|
+
currentAmbiguous !== nextAmbiguous
|
|
422
|
+
|| hasNumericDifference(currentAffinity, nextAffinity)
|
|
423
|
+
|| !areListsEqual(currentSubtags, nextSubtags);
|
|
424
|
+
|
|
425
|
+
if (!shouldUpdate) {
|
|
426
|
+
stats.skipped += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await updateSignals(assetId, {
|
|
431
|
+
llmSubtags: nextSubtags,
|
|
432
|
+
affinityWeight: nextAffinity,
|
|
433
|
+
ambiguous: nextAmbiguous,
|
|
434
|
+
});
|
|
435
|
+
stats.updated += 1;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
stats.failed += 1;
|
|
438
|
+
logger.warn('Falha na reclassificação semântica determinística.', {
|
|
439
|
+
action: 'sticker_semantic_reclassification_failed',
|
|
440
|
+
asset_id: assetId,
|
|
441
|
+
error: error?.message,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (stats.processed >= safeMaxItems) break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (rows.length < pageLimit) break;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return stats;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
export const deterministicReclassificationConfig = {
|
|
455
|
+
enabled: RECLASSIFICATION_ENABLED,
|
|
456
|
+
batch_size: RECLASSIFICATION_BATCH_SIZE,
|
|
457
|
+
max_per_cycle: RECLASSIFICATION_MAX_PER_CYCLE,
|
|
458
|
+
entropy_threshold: RECLASSIFICATION_ENTROPY_THRESHOLD,
|
|
459
|
+
affinity_threshold: RECLASSIFICATION_AFFINITY_THRESHOLD,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
export const __testablesSemanticReclassificationEngine = {
|
|
463
|
+
toSnakeCase,
|
|
464
|
+
normalizeTokenValue,
|
|
465
|
+
resolveSemanticGroup,
|
|
466
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyDictionaryMapping,
|
|
6
|
+
calculateCohesion,
|
|
7
|
+
detectConflict,
|
|
8
|
+
detectDominantTheme,
|
|
9
|
+
normalizeTokens,
|
|
10
|
+
reclassify,
|
|
11
|
+
} from './semanticReclassificationEngine.js';
|
|
12
|
+
|
|
13
|
+
test('normalizeTokens deve limpar termos genéricos, stopwords e duplicatas', () => {
|
|
14
|
+
const tokens = normalizeTokens({
|
|
15
|
+
llm_subtags: [
|
|
16
|
+
'Cute Anime Girl Sticker',
|
|
17
|
+
'cute anime girl',
|
|
18
|
+
'image',
|
|
19
|
+
'co',
|
|
20
|
+
'random art',
|
|
21
|
+
],
|
|
22
|
+
llm_style_traits: ['Kawaii style'],
|
|
23
|
+
llm_emotions: ['happy face'],
|
|
24
|
+
llm_pack_suggestions: ['social media picture'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const tokenMap = new Map(tokens.map((entry) => [entry.token, entry.weight]));
|
|
28
|
+
assert.equal(tokenMap.get('cute_anime_girl'), 2);
|
|
29
|
+
assert.equal(tokenMap.has('image'), false);
|
|
30
|
+
assert.equal(tokenMap.has('random_art'), false);
|
|
31
|
+
assert.equal(tokenMap.has('co'), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('applyDictionaryMapping deve mapear por correspondência parcial', () => {
|
|
35
|
+
const mapped = applyDictionaryMapping([
|
|
36
|
+
{ token: 'cute_anime_girl', weight: 1 },
|
|
37
|
+
{ token: 'chat_expression_face', weight: 1 },
|
|
38
|
+
{ token: 'meme_image_macro', weight: 1 },
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const mappedTokens = mapped.map((entry) => entry.token);
|
|
42
|
+
assert.ok(mappedTokens.includes('kawaii_anime_girl'));
|
|
43
|
+
assert.ok(mappedTokens.includes('chat_reaction'));
|
|
44
|
+
assert.ok(mappedTokens.includes('meme_reaction'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('detectDominantTheme e calculateCohesion devem priorizar tema com maior peso', () => {
|
|
48
|
+
const dominant = detectDominantTheme([
|
|
49
|
+
{ token: 'anime_hero', weight: 2 },
|
|
50
|
+
{ token: 'anime_smile', weight: 1 },
|
|
51
|
+
{ token: 'meme_reaction', weight: 1 },
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
assert.equal(dominant.dominant_theme, 'anime');
|
|
55
|
+
const cohesion = calculateCohesion({
|
|
56
|
+
dominantWeight: dominant.dominant_weight,
|
|
57
|
+
totalWeight: dominant.total_weight,
|
|
58
|
+
});
|
|
59
|
+
assert.equal(cohesion, 75);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('detectConflict deve marcar ambiguidade quando temas principais são próximos', () => {
|
|
63
|
+
const conflict = detectConflict({
|
|
64
|
+
themeWeights: new Map([
|
|
65
|
+
['kawaii', 3],
|
|
66
|
+
['horror', 2.7],
|
|
67
|
+
['anime', 0.5],
|
|
68
|
+
]),
|
|
69
|
+
totalWeight: 6.2,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert.equal(conflict.ambiguous, 1);
|
|
73
|
+
assert.equal(conflict.penalty_points, 20);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('reclassify deve manter apenas tokens do tema dominante e recalcular affinity', () => {
|
|
77
|
+
const output = reclassify({
|
|
78
|
+
llm_subtags: ['anime hero', 'anime smile', 'meme joke'],
|
|
79
|
+
llm_style_traits: ['anime expression'],
|
|
80
|
+
llm_emotions: ['chat expression'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
assert.equal(output.dominant_theme, 'anime');
|
|
84
|
+
assert.equal(output.ambiguous, 0);
|
|
85
|
+
assert.ok(output.cohesion_score >= 60);
|
|
86
|
+
assert.ok(output.updated_affinity_weight >= 0.6);
|
|
87
|
+
assert.deepEqual(output.normalized_subtags, ['anime_expression', 'anime_hero', 'anime_smile']);
|
|
88
|
+
});
|