@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.
Files changed (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,449 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const parseJson = (value, fallback = null) => {
4
+ if (value === null || value === undefined) return fallback;
5
+ if (typeof value === 'object') return value;
6
+ if (Buffer.isBuffer(value)) {
7
+ try {
8
+ return JSON.parse(value.toString('utf8'));
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+
14
+ if (typeof value === 'string') {
15
+ try {
16
+ return JSON.parse(value);
17
+ } catch {
18
+ return fallback;
19
+ }
20
+ }
21
+
22
+ return fallback;
23
+ };
24
+
25
+ const clampNumber = (value, min, max) => Math.max(min, Math.min(max, Number(value)));
26
+
27
+ const deriveEntropyNormalized = (entropyValue, topLabels = []) => {
28
+ const entropy = Number(entropyValue);
29
+ if (!Number.isFinite(entropy) || entropy <= 0) return 0;
30
+ const k = Array.isArray(topLabels) ? topLabels.length : 0;
31
+ if (k > 1) {
32
+ const maxEntropy = Math.log(k);
33
+ if (maxEntropy > 0) return clampNumber(entropy / maxEntropy, 0, 1);
34
+ }
35
+ const legacyThreshold = 2.5;
36
+ return clampNumber(entropy / legacyThreshold, 0, 1);
37
+ };
38
+
39
+ const normalizeClassificationRow = (row) => {
40
+ if (!row) return null;
41
+
42
+ const topLabels = parseJson(row.top_labels, []);
43
+ const similarImages = parseJson(row.similar_images, []);
44
+ const llmSubtags = parseJson(row.llm_subtags, []);
45
+ const llmStyleTraits = parseJson(row.llm_style_traits, []);
46
+ const llmEmotions = parseJson(row.llm_emotions, []);
47
+ const llmPackSuggestions = parseJson(row.llm_pack_suggestions, []);
48
+ const entropy = row.entropy !== null && row.entropy !== undefined ? Number(row.entropy) : null;
49
+
50
+ return {
51
+ asset_id: row.asset_id,
52
+ provider: row.provider || 'clip',
53
+ model_name: row.model_name || null,
54
+ classification_version: row.classification_version || 'v1',
55
+ category: row.category || null,
56
+ confidence: row.confidence !== null && row.confidence !== undefined ? Number(row.confidence) : null,
57
+ entropy,
58
+ entropy_normalized: entropy !== null ? Number(deriveEntropyNormalized(entropy, topLabels).toFixed(6)) : null,
59
+ confidence_margin: row.confidence_margin !== null && row.confidence_margin !== undefined ? Number(row.confidence_margin) : null,
60
+ affinity_weight: row.affinity_weight !== null && row.affinity_weight !== undefined ? Number(row.affinity_weight) : null,
61
+ nsfw_score: row.nsfw_score !== null && row.nsfw_score !== undefined ? Number(row.nsfw_score) : null,
62
+ is_nsfw: row.is_nsfw === 1 || row.is_nsfw === true,
63
+ ambiguous: row.ambiguous === 1 || row.ambiguous === true,
64
+ image_hash: row.image_hash || null,
65
+ all_scores: parseJson(row.all_scores, {}),
66
+ top_labels: Array.isArray(topLabels) ? topLabels : [],
67
+ similar_images: Array.isArray(similarImages) ? similarImages : [],
68
+ llm_subtags: Array.isArray(llmSubtags) ? llmSubtags : [],
69
+ llm_style_traits: Array.isArray(llmStyleTraits) ? llmStyleTraits : [],
70
+ llm_emotions: Array.isArray(llmEmotions) ? llmEmotions : [],
71
+ llm_pack_suggestions: Array.isArray(llmPackSuggestions) ? llmPackSuggestions : [],
72
+ semantic_cluster_id:
73
+ row.semantic_cluster_id !== null && row.semantic_cluster_id !== undefined
74
+ ? Number(row.semantic_cluster_id)
75
+ : null,
76
+ semantic_cluster_slug: row.semantic_cluster_slug || null,
77
+ classified_at: row.classified_at,
78
+ updated_at: row.updated_at,
79
+ };
80
+ };
81
+
82
+ export async function findStickerClassificationByAssetId(assetId, connection = null) {
83
+ const rows = await executeQuery(
84
+ `SELECT * FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} WHERE asset_id = ? LIMIT 1`,
85
+ [assetId],
86
+ connection,
87
+ );
88
+
89
+ return normalizeClassificationRow(rows?.[0] || null);
90
+ }
91
+
92
+ export async function listStickerClassificationsByAssetIds(assetIds, connection = null) {
93
+ if (!Array.isArray(assetIds) || !assetIds.length) return [];
94
+
95
+ const uniqueIds = Array.from(new Set(assetIds.filter(Boolean)));
96
+ if (!uniqueIds.length) return [];
97
+
98
+ const placeholders = uniqueIds.map(() => '?').join(', ');
99
+ const rows = await executeQuery(
100
+ `SELECT * FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} WHERE asset_id IN (${placeholders})`,
101
+ uniqueIds,
102
+ connection,
103
+ );
104
+
105
+ const normalized = rows.map((row) => normalizeClassificationRow(row));
106
+ const byAssetId = new Map(normalized.map((entry) => [entry.asset_id, entry]));
107
+ return uniqueIds.map((assetId) => byAssetId.get(assetId)).filter(Boolean);
108
+ }
109
+
110
+ export async function upsertStickerAssetClassification(payload, connection = null) {
111
+ await executeQuery(
112
+ `INSERT INTO ${TABLES.STICKER_ASSET_CLASSIFICATION}
113
+ (asset_id, provider, model_name, classification_version, category, confidence, entropy, confidence_margin, nsfw_score, is_nsfw, all_scores, top_labels, affinity_weight, image_hash, ambiguous, llm_subtags, llm_style_traits, llm_emotions, llm_pack_suggestions, semantic_cluster_id, semantic_cluster_slug, similar_images, classified_at)
114
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
115
+ ON DUPLICATE KEY UPDATE
116
+ provider = VALUES(provider),
117
+ model_name = VALUES(model_name),
118
+ classification_version = VALUES(classification_version),
119
+ category = VALUES(category),
120
+ confidence = VALUES(confidence),
121
+ entropy = VALUES(entropy),
122
+ confidence_margin = VALUES(confidence_margin),
123
+ nsfw_score = VALUES(nsfw_score),
124
+ is_nsfw = VALUES(is_nsfw),
125
+ all_scores = VALUES(all_scores),
126
+ top_labels = VALUES(top_labels),
127
+ affinity_weight = VALUES(affinity_weight),
128
+ image_hash = VALUES(image_hash),
129
+ ambiguous = VALUES(ambiguous),
130
+ llm_subtags = VALUES(llm_subtags),
131
+ llm_style_traits = VALUES(llm_style_traits),
132
+ llm_emotions = VALUES(llm_emotions),
133
+ llm_pack_suggestions = VALUES(llm_pack_suggestions),
134
+ semantic_cluster_id = VALUES(semantic_cluster_id),
135
+ semantic_cluster_slug = VALUES(semantic_cluster_slug),
136
+ similar_images = VALUES(similar_images),
137
+ classified_at = CURRENT_TIMESTAMP,
138
+ updated_at = CURRENT_TIMESTAMP`,
139
+ [
140
+ payload.asset_id,
141
+ payload.provider || 'clip',
142
+ payload.model_name || null,
143
+ payload.classification_version || 'v1',
144
+ payload.category || null,
145
+ payload.confidence ?? null,
146
+ payload.entropy ?? null,
147
+ payload.confidence_margin ?? null,
148
+ payload.nsfw_score ?? null,
149
+ payload.is_nsfw ? 1 : 0,
150
+ payload.all_scores ? JSON.stringify(payload.all_scores) : JSON.stringify({}),
151
+ payload.top_labels ? JSON.stringify(payload.top_labels) : JSON.stringify([]),
152
+ payload.affinity_weight ?? null,
153
+ payload.image_hash || null,
154
+ payload.ambiguous ? 1 : 0,
155
+ payload.llm_subtags ? JSON.stringify(payload.llm_subtags) : JSON.stringify([]),
156
+ payload.llm_style_traits ? JSON.stringify(payload.llm_style_traits) : JSON.stringify([]),
157
+ payload.llm_emotions ? JSON.stringify(payload.llm_emotions) : JSON.stringify([]),
158
+ payload.llm_pack_suggestions ? JSON.stringify(payload.llm_pack_suggestions) : JSON.stringify([]),
159
+ payload.semantic_cluster_id ?? null,
160
+ payload.semantic_cluster_slug || null,
161
+ payload.similar_images ? JSON.stringify(payload.similar_images) : JSON.stringify([]),
162
+ ],
163
+ connection,
164
+ );
165
+
166
+ return findStickerClassificationByAssetId(payload.asset_id, connection);
167
+ }
168
+
169
+ export async function updateStickerClassificationSemanticCluster(
170
+ assetId,
171
+ { semanticClusterId = null, semanticClusterSlug = null } = {},
172
+ connection = null,
173
+ ) {
174
+ if (!assetId) return null;
175
+
176
+ await executeQuery(
177
+ `UPDATE ${TABLES.STICKER_ASSET_CLASSIFICATION}
178
+ SET semantic_cluster_id = ?, semantic_cluster_slug = ?, updated_at = CURRENT_TIMESTAMP
179
+ WHERE asset_id = ?`,
180
+ [
181
+ semanticClusterId ?? null,
182
+ semanticClusterSlug || null,
183
+ assetId,
184
+ ],
185
+ connection,
186
+ );
187
+
188
+ return findStickerClassificationByAssetId(assetId, connection);
189
+ }
190
+
191
+ export async function listClipImageEmbeddingsByImageHashes(imageHashes, connection = null) {
192
+ const uniqueHashes = Array.from(
193
+ new Set((Array.isArray(imageHashes) ? imageHashes : [])
194
+ .map((value) => String(value || '').trim().toLowerCase())
195
+ .filter((value) => value.length === 64)),
196
+ );
197
+ if (!uniqueHashes.length) return [];
198
+
199
+ const placeholders = uniqueHashes.map(() => '?').join(', ');
200
+ try {
201
+ const rows = await executeQuery(
202
+ `SELECT image_hash, embedding, embedding_dim
203
+ FROM clip_image_embedding_cache
204
+ WHERE image_hash IN (${placeholders})`,
205
+ uniqueHashes,
206
+ connection,
207
+ );
208
+ return Array.isArray(rows) ? rows : [];
209
+ } catch {
210
+ return [];
211
+ }
212
+ }
213
+
214
+ export async function deleteStickerAssetClassificationByAssetId(assetId, connection = null) {
215
+ const result = await executeQuery(
216
+ `DELETE FROM ${TABLES.STICKER_ASSET_CLASSIFICATION}
217
+ WHERE asset_id = ?`,
218
+ [assetId],
219
+ connection,
220
+ );
221
+
222
+ return Number(result?.affectedRows || 0);
223
+ }
224
+
225
+ const clampInt = (value, fallback, min, max) => {
226
+ const numeric = Number(value);
227
+ if (!Number.isFinite(numeric)) return fallback;
228
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
229
+ };
230
+
231
+ export async function listAssetsForModelUpgradeReprocess(
232
+ { currentVersion, limit = 150, offset = 0 } = {},
233
+ connection = null,
234
+ ) {
235
+ const normalizedVersion = String(currentVersion || '').trim();
236
+ if (!normalizedVersion) return [];
237
+
238
+ const safeLimit = clampInt(limit, 150, 1, 1000);
239
+ const safeOffset = clampInt(offset, 0, 0, 500000);
240
+ const rows = await executeQuery(
241
+ `SELECT c.asset_id
242
+ FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} c
243
+ LEFT JOIN ${TABLES.STICKER_ASSET_REPROCESS_QUEUE} q
244
+ ON q.asset_id = c.asset_id
245
+ AND q.reason = 'MODEL_UPGRADE'
246
+ AND q.status IN ('pending', 'processing')
247
+ WHERE c.classification_version <> ?
248
+ AND q.id IS NULL
249
+ ORDER BY c.updated_at ASC
250
+ LIMIT ${safeLimit} OFFSET ${safeOffset}`,
251
+ [normalizedVersion],
252
+ connection,
253
+ );
254
+
255
+ return rows.map((row) => row.asset_id).filter(Boolean);
256
+ }
257
+
258
+ export async function listAssetsForLowConfidenceReprocess(
259
+ { confidenceThreshold = 0.65, staleHours = 48, limit = 150, offset = 0 } = {},
260
+ connection = null,
261
+ ) {
262
+ const threshold = Number(confidenceThreshold);
263
+ if (!Number.isFinite(threshold)) return [];
264
+
265
+ const safeStaleHours = clampInt(staleHours, 48, 1, 24 * 365);
266
+ const safeLimit = clampInt(limit, 150, 1, 1000);
267
+ const safeOffset = clampInt(offset, 0, 0, 500000);
268
+
269
+ const rows = await executeQuery(
270
+ `SELECT c.asset_id
271
+ FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} c
272
+ LEFT JOIN ${TABLES.STICKER_ASSET_REPROCESS_QUEUE} q
273
+ ON q.asset_id = c.asset_id
274
+ AND q.reason = 'LOW_CONFIDENCE'
275
+ AND q.status IN ('pending', 'processing')
276
+ WHERE c.confidence IS NOT NULL
277
+ AND c.confidence < ?
278
+ AND c.updated_at <= (UTC_TIMESTAMP() - INTERVAL ${safeStaleHours} HOUR)
279
+ AND q.id IS NULL
280
+ ORDER BY c.confidence ASC, c.updated_at ASC
281
+ LIMIT ${safeLimit} OFFSET ${safeOffset}`,
282
+ [threshold],
283
+ connection,
284
+ );
285
+
286
+ return rows.map((row) => row.asset_id).filter(Boolean);
287
+ }
288
+
289
+ export async function listAssetsForPrioritySignalBackfillReprocess(
290
+ { limit = 200, offset = 0 } = {},
291
+ connection = null,
292
+ ) {
293
+ const safeLimit = clampInt(limit, 200, 1, 2000);
294
+ const safeOffset = clampInt(offset, 0, 0, 500000);
295
+ const rows = await executeQuery(
296
+ `SELECT
297
+ c.asset_id,
298
+ MAX(
299
+ (CASE WHEN c.category IS NULL OR TRIM(c.category) = '' THEN 1 ELSE 0 END)
300
+ + (CASE WHEN c.confidence IS NULL THEN 1 ELSE 0 END)
301
+ + (CASE WHEN c.entropy IS NULL THEN 1 ELSE 0 END)
302
+ + (CASE WHEN c.confidence_margin IS NULL THEN 1 ELSE 0 END)
303
+ + (CASE WHEN c.affinity_weight IS NULL THEN 1 ELSE 0 END)
304
+ + (CASE WHEN c.image_hash IS NULL OR TRIM(c.image_hash) = '' THEN 1 ELSE 0 END)
305
+ + (CASE WHEN COALESCE(JSON_LENGTH(c.all_scores), 0) = 0 THEN 1 ELSE 0 END)
306
+ + (CASE WHEN COALESCE(JSON_LENGTH(c.top_labels), 0) = 0 THEN 1 ELSE 0 END)
307
+ + (CASE WHEN COALESCE(JSON_LENGTH(c.llm_subtags), 0) = 0 THEN 1 ELSE 0 END)
308
+ + (CASE WHEN COALESCE(JSON_LENGTH(c.llm_style_traits), 0) = 0 THEN 1 ELSE 0 END)
309
+ + (CASE WHEN COALESCE(JSON_LENGTH(c.llm_emotions), 0) = 0 THEN 1 ELSE 0 END)
310
+ + (CASE WHEN COALESCE(JSON_LENGTH(c.llm_pack_suggestions), 0) = 0 THEN 1 ELSE 0 END)
311
+ + (CASE WHEN c.semantic_cluster_id IS NULL THEN 1 ELSE 0 END)
312
+ + (CASE WHEN c.semantic_cluster_slug IS NULL OR TRIM(c.semantic_cluster_slug) = '' THEN 1 ELSE 0 END)
313
+ ) AS missing_signal_score
314
+ FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} c
315
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.sticker_id = c.asset_id
316
+ LEFT JOIN ${TABLES.STICKER_PACK} p ON p.id = i.pack_id AND p.deleted_at IS NULL
317
+ LEFT JOIN ${TABLES.STICKER_PACK_ENGAGEMENT} e ON e.pack_id = p.id
318
+ LEFT JOIN ${TABLES.STICKER_ASSET_REPROCESS_QUEUE} q
319
+ ON q.asset_id = c.asset_id
320
+ AND q.reason = 'MODEL_UPGRADE'
321
+ AND q.status IN ('pending', 'processing')
322
+ WHERE q.id IS NULL
323
+ GROUP BY c.asset_id
324
+ HAVING missing_signal_score > 0
325
+ ORDER BY
326
+ missing_signal_score DESC,
327
+ MAX(CASE WHEN p.pack_status = 'ready' THEN 1 ELSE 0 END) DESC,
328
+ MAX(CASE WHEN p.visibility = 'public' THEN 1 ELSE 0 END) DESC,
329
+ MAX(COALESCE(e.like_count, 0) + COALESCE(e.open_count, 0) * 0.02) DESC,
330
+ COUNT(i.pack_id) DESC,
331
+ MAX(c.updated_at) ASC
332
+ LIMIT ${safeLimit} OFFSET ${safeOffset}`,
333
+ [],
334
+ connection,
335
+ );
336
+
337
+ return rows.map((row) => row.asset_id).filter(Boolean);
338
+ }
339
+
340
+ export async function listStickerClassificationsForDeterministicReprocess(
341
+ {
342
+ limit = 250,
343
+ cursorAssetId = '',
344
+ entropyThreshold = 0.8,
345
+ affinityThreshold = 0.3,
346
+ } = {},
347
+ connection = null,
348
+ ) {
349
+ const safeLimit = clampInt(limit, 250, 1, 2000);
350
+ const normalizedCursor = String(cursorAssetId || '').trim();
351
+ const normalizedEntropyThreshold = Number.isFinite(Number(entropyThreshold))
352
+ ? Number(entropyThreshold)
353
+ : 0.8;
354
+ const normalizedAffinityThreshold = Number.isFinite(Number(affinityThreshold))
355
+ ? Number(affinityThreshold)
356
+ : 0.3;
357
+
358
+ const params = [normalizedEntropyThreshold, normalizedAffinityThreshold];
359
+ const cursorClause = normalizedCursor ? 'AND c.asset_id > ?' : '';
360
+ if (normalizedCursor) {
361
+ params.push(normalizedCursor);
362
+ }
363
+
364
+ const rows = await executeQuery(
365
+ `SELECT
366
+ c.asset_id,
367
+ c.top_labels,
368
+ c.llm_subtags,
369
+ c.llm_style_traits,
370
+ c.llm_emotions,
371
+ c.llm_pack_suggestions,
372
+ c.affinity_weight,
373
+ c.entropy,
374
+ c.ambiguous
375
+ FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} c
376
+ WHERE (
377
+ c.ambiguous = 1
378
+ OR COALESCE(c.entropy, 0) > ?
379
+ OR COALESCE(c.affinity_weight, 0) < ?
380
+ )
381
+ ${cursorClause}
382
+ ORDER BY c.asset_id ASC
383
+ LIMIT ${safeLimit}`,
384
+ params,
385
+ connection,
386
+ );
387
+
388
+ return rows.map((row) => normalizeClassificationRow(row));
389
+ }
390
+
391
+ export async function updateStickerClassificationDeterministicSignals(
392
+ assetId,
393
+ { llmSubtags = [], affinityWeight = null, ambiguous = 0 } = {},
394
+ connection = null,
395
+ ) {
396
+ if (!assetId) return null;
397
+ const normalizedAffinityWeight = Number.isFinite(Number(affinityWeight))
398
+ ? Math.max(0, Math.min(1, Number(affinityWeight)))
399
+ : null;
400
+ const normalizedAmbiguous = ambiguous === 1 || ambiguous === true ? 1 : 0;
401
+ const normalizedSubtags = Array.isArray(llmSubtags)
402
+ ? llmSubtags.map((value) => String(value || '').trim()).filter(Boolean)
403
+ : [];
404
+
405
+ await executeQuery(
406
+ `UPDATE ${TABLES.STICKER_ASSET_CLASSIFICATION}
407
+ SET llm_subtags = ?, affinity_weight = ?, ambiguous = ?, updated_at = CURRENT_TIMESTAMP
408
+ WHERE asset_id = ?`,
409
+ [
410
+ JSON.stringify(normalizedSubtags),
411
+ normalizedAffinityWeight,
412
+ normalizedAmbiguous,
413
+ assetId,
414
+ ],
415
+ connection,
416
+ );
417
+
418
+ return findStickerClassificationByAssetId(assetId, connection);
419
+ }
420
+
421
+ export async function listClassificationCategoryDistribution({ days = 7 } = {}, connection = null) {
422
+ const safeDays = clampInt(days, 7, 1, 365);
423
+ const rows = await executeQuery(
424
+ `SELECT
425
+ LOWER(TRIM(COALESCE(category, 'unknown'))) AS category,
426
+ COUNT(*) AS total
427
+ FROM ${TABLES.STICKER_ASSET_CLASSIFICATION}
428
+ WHERE updated_at >= (UTC_TIMESTAMP() - INTERVAL ${safeDays} DAY)
429
+ GROUP BY LOWER(TRIM(COALESCE(category, 'unknown')))`,
430
+ [],
431
+ connection,
432
+ );
433
+
434
+ const distribution = new Map();
435
+ let total = 0;
436
+ for (const row of rows) {
437
+ const category = String(row.category || 'unknown').trim() || 'unknown';
438
+ const count = Number(row.total || 0);
439
+ if (!count) continue;
440
+ distribution.set(category, count);
441
+ total += count;
442
+ }
443
+
444
+ return {
445
+ days: safeDays,
446
+ total,
447
+ categories: distribution,
448
+ };
449
+ }