@kaikybrofc/omnizap-system 2.2.9 → 2.3.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.
Files changed (121) hide show
  1. package/README.md +20 -18
  2. package/app/config/adminIdentity.js +1 -3
  3. package/app/connection/socketController.js +10 -20
  4. package/app/controllers/messageController.js +7 -28
  5. package/app/modules/aiModule/catCommand.js +29 -192
  6. package/app/modules/broadcastModule/noticeCommand.js +28 -97
  7. package/app/modules/gameModule/diceCommand.js +6 -32
  8. package/app/modules/playModule/playCommand.js +57 -258
  9. package/app/modules/quoteModule/quoteCommand.js +2 -4
  10. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1 -13
  11. package/app/modules/statsModule/noMessageCommand.js +16 -84
  12. package/app/modules/statsModule/rankingCommand.js +5 -25
  13. package/app/modules/statsModule/rankingCommon.js +1 -9
  14. package/app/modules/stickerModule/convertToWebp.js +4 -27
  15. package/app/modules/stickerModule/stickerCommand.js +13 -24
  16. package/app/modules/stickerModule/stickerTextCommand.js +13 -25
  17. package/app/modules/stickerPackModule/autoPackCollectorService.js +16 -7
  18. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +20 -36
  19. package/app/modules/stickerPackModule/domainEvents.js +2 -11
  20. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +13 -50
  21. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +2 -15
  22. package/app/modules/stickerPackModule/semanticThemeClusterService.js +14 -41
  23. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +25 -95
  24. package/app/modules/stickerPackModule/stickerAssetRepository.js +12 -31
  25. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +13 -18
  26. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +284 -709
  27. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +27 -106
  28. package/app/modules/stickerPackModule/stickerClassificationService.js +46 -77
  29. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +13 -53
  30. package/app/modules/stickerPackModule/stickerDomainEventBus.js +10 -16
  31. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +13 -34
  32. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +1 -4
  33. package/app/modules/stickerPackModule/stickerObjectStorageService.js +26 -26
  34. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +32 -187
  35. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +6 -15
  36. package/app/modules/stickerPackModule/stickerPackItemRepository.js +6 -32
  37. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +12 -36
  38. package/app/modules/stickerPackModule/stickerPackMessageService.js +12 -40
  39. package/app/modules/stickerPackModule/stickerPackRepository.js +23 -66
  40. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +9 -21
  41. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +10 -40
  42. package/app/modules/stickerPackModule/stickerPackService.js +50 -115
  43. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +2 -21
  44. package/app/modules/stickerPackModule/stickerPackUtils.js +13 -3
  45. package/app/modules/stickerPackModule/stickerStorageService.js +16 -65
  46. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +4 -22
  47. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +14 -29
  48. package/app/modules/systemMetricsModule/pingCommand.js +9 -39
  49. package/app/modules/tiktokModule/tiktokCommand.js +17 -109
  50. package/app/modules/userModule/userCommand.js +2 -88
  51. package/app/observability/metrics.js +5 -16
  52. package/app/services/captchaService.js +1 -6
  53. package/app/services/dbWriteQueue.js +3 -18
  54. package/app/services/featureFlagService.js +2 -8
  55. package/app/services/newsBroadcastService.js +0 -1
  56. package/app/services/queueUtils.js +2 -4
  57. package/app/services/whatsappLoginLinkService.js +7 -9
  58. package/app/store/premiumUserStore.js +1 -2
  59. package/app/utils/antiLink/antiLinkModule.js +3 -233
  60. package/app/utils/logger/loggerModule.js +9 -34
  61. package/app/utils/systemMetrics/systemMetricsModule.js +1 -4
  62. package/database/init.js +1 -8
  63. package/docker-compose.yml +27 -27
  64. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +220 -0
  65. package/docs/seo/satellite-page-template.md +91 -0
  66. package/docs/seo/satellite-pages-phase1.json +349 -0
  67. package/eslint.config.js +2 -15
  68. package/index.js +8 -36
  69. package/ml/clip_classifier/README.md +4 -6
  70. package/observability/alert-rules.yml +12 -12
  71. package/observability/grafana/provisioning/dashboards/dashboards.yml +1 -1
  72. package/package.json +8 -3
  73. package/public/api-docs/index.html +224 -141
  74. package/public/bot-whatsapp-para-grupo/index.html +306 -0
  75. package/public/bot-whatsapp-sem-programar/index.html +306 -0
  76. package/public/comandos/index.html +428 -0
  77. package/public/como-automatizar-avisos-no-whatsapp/index.html +306 -0
  78. package/public/como-criar-comandos-whatsapp/index.html +306 -0
  79. package/public/como-evitar-spam-no-whatsapp/index.html +306 -0
  80. package/public/como-moderar-grupo-whatsapp/index.html +306 -0
  81. package/public/como-organizar-comunidade-whatsapp/index.html +306 -0
  82. package/public/css/github-project-panel.css +20 -15
  83. package/public/css/stickers-admin.css +55 -39
  84. package/public/css/styles.css +37 -29
  85. package/public/index.html +1060 -1417
  86. package/public/js/apps/apiDocsApp.js +36 -153
  87. package/public/js/apps/createPackApp.js +69 -332
  88. package/public/js/apps/homeApp.js +201 -434
  89. package/public/js/apps/loginApp.js +3 -12
  90. package/public/js/apps/stickersAdminApp.js +190 -181
  91. package/public/js/apps/stickersApp.js +507 -1366
  92. package/public/js/catalog.js +11 -74
  93. package/public/js/github-panel/components/ErrorState.js +1 -8
  94. package/public/js/github-panel/components/GithubProjectPanel.js +2 -9
  95. package/public/js/github-panel/components/SkeletonPanel.js +1 -11
  96. package/public/js/github-panel/components/StatCard.js +1 -7
  97. package/public/js/github-panel/vendor/react.js +1 -9
  98. package/public/js/runtime/react-runtime.js +1 -9
  99. package/public/licenca/index.html +104 -86
  100. package/public/login/index.html +315 -321
  101. package/public/melhor-bot-whatsapp-para-grupos/index.html +306 -0
  102. package/public/sitemap.xml +45 -0
  103. package/public/stickers/admin/index.html +14 -19
  104. package/public/stickers/create/index.html +39 -43
  105. package/public/stickers/index.html +97 -41
  106. package/public/termos-de-uso/index.html +142 -115
  107. package/public/user/index.html +347 -346
  108. package/scripts/cache-bust.mjs +5 -24
  109. package/scripts/generate-seo-satellite-pages.mjs +431 -0
  110. package/scripts/run-prettier-all.mjs +25 -0
  111. package/scripts/sticker-catalog-loadtest.mjs +13 -11
  112. package/scripts/sticker-worker-task.mjs +1 -4
  113. package/scripts/sync-readme-snapshot.mjs +3 -2
  114. package/server/controllers/stickerCatalogController.js +407 -704
  115. package/server/http/httpServer.js +2 -10
  116. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +1 -8
  117. package/server/routes/stickerCatalog/catalogHandlers/catalogAuthHttp.js +1 -9
  118. package/server/routes/stickerCatalog/catalogHandlers/catalogPublicHttp.js +10 -11
  119. package/server/routes/stickerCatalog/catalogHandlers/catalogUploadHttp.js +1 -10
  120. package/server/routes/stickerCatalog/catalogRouter.js +11 -13
  121. package/kaikybrofc-omnizap-system-2.2.9.tgz +0 -0
@@ -11,40 +11,15 @@ const parseEnvBool = (value, fallback) => {
11
11
  const clampNumber = (value, min, max) => Math.max(min, Math.min(max, Number(value)));
12
12
 
13
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
- ];
14
+ const RECLASSIFICATION_BATCH_SIZE = Math.max(50, Math.min(2000, Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_BATCH_SIZE) || 400));
15
+ const RECLASSIFICATION_MAX_PER_CYCLE = Math.max(100, Math.min(20_000, Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_MAX_PER_CYCLE) || 2000));
16
+ const RECLASSIFICATION_ENTROPY_THRESHOLD = Number.isFinite(Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_ENTROPY_THRESHOLD)) ? Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_ENTROPY_THRESHOLD) : 0.8;
17
+ const RECLASSIFICATION_AFFINITY_THRESHOLD = Number.isFinite(Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_AFFINITY_THRESHOLD)) ? Number(process.env.STICKER_SEMANTIC_RECLASSIFICATION_AFFINITY_THRESHOLD) : 0.3;
18
+
19
+ const STOPWORDS = ['image', 'sticker', 'wallpaper', 'social_media', 'internet', 'picture'];
20
+ const GENERIC_TERMS = ['cool', 'nice', 'funny', 'random', 'art'];
44
21
  const SEMANTIC_GROUPS = ['anime', 'meme', 'kawaii', 'horror', 'reaction'];
45
- const OPPOSITE_THEME_PAIRS = [
46
- ['kawaii', 'horror'],
47
- ];
22
+ const OPPOSITE_THEME_PAIRS = [['kawaii', 'horror']];
48
23
 
49
24
  const DICTIONARY_MAP = {
50
25
  'cute anime girl': 'kawaii_anime_girl',
@@ -66,15 +41,13 @@ const toSnakeCase = (value) =>
66
41
 
67
42
  const STOPWORD_PHRASES = new Set(STOPWORDS.map((value) => toSnakeCase(value)).filter(Boolean));
68
43
  const STOPWORD_WORDS = new Set(
69
- STOPWORDS
70
- .map((value) => toSnakeCase(value))
44
+ STOPWORDS.map((value) => toSnakeCase(value))
71
45
  .flatMap((value) => value.split('_'))
72
46
  .filter((value) => value.length >= 3),
73
47
  );
74
48
  const GENERIC_TERM_SET = new Set(GENERIC_TERMS.map((value) => toSnakeCase(value)).filter(Boolean));
75
49
  const GENERIC_TERM_WORDS = new Set(
76
- GENERIC_TERMS
77
- .map((value) => toSnakeCase(value))
50
+ GENERIC_TERMS.map((value) => toSnakeCase(value))
78
51
  .flatMap((value) => value.split('_'))
79
52
  .filter((value) => value.length >= 3),
80
53
  );
@@ -342,9 +315,7 @@ export const reclassify = (classification = {}) => {
342
315
  dominantTokens = mappedTokens.slice();
343
316
  }
344
317
 
345
- const normalizedSubtags = dominantTokens
346
- .map((entry) => normalizeTokenValue(entry.token))
347
- .filter(Boolean);
318
+ const normalizedSubtags = dominantTokens.map((entry) => normalizeTokenValue(entry.token)).filter(Boolean);
348
319
 
349
320
  const outputSubtags = Array.from(new Set(normalizedSubtags));
350
321
  const updatedAffinityWeight = Number(clampNumber(cohesionScore / 100, 0, 1).toFixed(6));
@@ -358,12 +329,7 @@ export const reclassify = (classification = {}) => {
358
329
  };
359
330
  };
360
331
 
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
- } = {}) => {
332
+ export const batchReprocess = async ({ maxItems = RECLASSIFICATION_MAX_PER_CYCLE, batchSize = RECLASSIFICATION_BATCH_SIZE, entropyThreshold = RECLASSIFICATION_ENTROPY_THRESHOLD, affinityThreshold = RECLASSIFICATION_AFFINITY_THRESHOLD } = {}) => {
367
333
  const safeMaxItems = Math.max(0, Math.min(50_000, Number(maxItems) || RECLASSIFICATION_MAX_PER_CYCLE));
368
334
  const safeBatchSize = Math.max(1, Math.min(2000, Number(batchSize) || RECLASSIFICATION_BATCH_SIZE));
369
335
 
@@ -417,10 +383,7 @@ export const batchReprocess = async ({
417
383
  const currentAmbiguous = row?.ambiguous ? 1 : 0;
418
384
  const nextAmbiguous = output.ambiguous ? 1 : 0;
419
385
 
420
- const shouldUpdate =
421
- currentAmbiguous !== nextAmbiguous
422
- || hasNumericDifference(currentAffinity, nextAffinity)
423
- || !areListsEqual(currentSubtags, nextSubtags);
386
+ const shouldUpdate = currentAmbiguous !== nextAmbiguous || hasNumericDifference(currentAffinity, nextAffinity) || !areListsEqual(currentSubtags, nextSubtags);
424
387
 
425
388
  if (!shouldUpdate) {
426
389
  stats.skipped += 1;
@@ -1,24 +1,11 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
 
4
- import {
5
- applyDictionaryMapping,
6
- calculateCohesion,
7
- detectConflict,
8
- detectDominantTheme,
9
- normalizeTokens,
10
- reclassify,
11
- } from './semanticReclassificationEngine.js';
4
+ import { applyDictionaryMapping, calculateCohesion, detectConflict, detectDominantTheme, normalizeTokens, reclassify } from './semanticReclassificationEngine.js';
12
5
 
13
6
  test('normalizeTokens deve limpar termos genéricos, stopwords e duplicatas', () => {
14
7
  const tokens = normalizeTokens({
15
- llm_subtags: [
16
- 'Cute Anime Girl Sticker',
17
- 'cute anime girl',
18
- 'image',
19
- 'co',
20
- 'random art',
21
- ],
8
+ llm_subtags: ['Cute Anime Girl Sticker', 'cute anime girl', 'image', 'co', 'random art'],
22
9
  llm_style_traits: ['Kawaii style'],
23
10
  llm_emotions: ['happy face'],
24
11
  llm_pack_suggestions: ['social media picture'],
@@ -4,10 +4,7 @@ import OpenAI from 'openai';
4
4
 
5
5
  import { executeQuery, TABLES } from '../../../database/index.js';
6
6
  import logger from '../../utils/logger/loggerModule.js';
7
- import {
8
- findStickerClassificationByAssetId,
9
- updateStickerClassificationSemanticCluster,
10
- } from './stickerAssetClassificationRepository.js';
7
+ import { findStickerClassificationByAssetId, updateStickerClassificationSemanticCluster } from './stickerAssetClassificationRepository.js';
11
8
 
12
9
  const parseEnvBool = (value, fallback) => {
13
10
  if (value === undefined || value === null || value === '') return fallback;
@@ -19,12 +16,9 @@ const parseEnvBool = (value, fallback) => {
19
16
 
20
17
  const ENABLE_SEMANTIC_CLUSTERING = parseEnvBool(process.env.ENABLE_SEMANTIC_CLUSTERING, false);
21
18
  const OPENAI_TIMEOUT_MS = Math.max(1_000, Number(process.env.SEMANTIC_CLUSTER_OPENAI_TIMEOUT_MS) || 10_000);
22
- const EMBEDDING_MODEL = String(process.env.SEMANTIC_CLUSTER_EMBEDDING_MODEL || 'text-embedding-3-small').trim()
23
- || 'text-embedding-3-small';
19
+ const EMBEDDING_MODEL = String(process.env.SEMANTIC_CLUSTER_EMBEDDING_MODEL || 'text-embedding-3-small').trim() || 'text-embedding-3-small';
24
20
  const SLUG_MODEL = String(process.env.SEMANTIC_CLUSTER_SLUG_MODEL || 'gpt-4o-mini').trim() || 'gpt-4o-mini';
25
- const SIMILARITY_THRESHOLD = Number.isFinite(Number(process.env.SEMANTIC_CLUSTER_SIMILARITY_THRESHOLD))
26
- ? Math.max(0.5, Math.min(0.99, Number(process.env.SEMANTIC_CLUSTER_SIMILARITY_THRESHOLD)))
27
- : 0.87;
21
+ const SIMILARITY_THRESHOLD = Number.isFinite(Number(process.env.SEMANTIC_CLUSTER_SIMILARITY_THRESHOLD)) ? Math.max(0.5, Math.min(0.99, Number(process.env.SEMANTIC_CLUSTER_SIMILARITY_THRESHOLD))) : 0.87;
28
22
  const MAX_CLUSTER_SCAN = Math.max(100, Math.min(20_000, Number(process.env.SEMANTIC_CLUSTER_MAX_SCAN) || 5_000));
29
23
  const MAX_SUGGESTIONS_PER_ASSET = Math.max(1, Math.min(20, Number(process.env.SEMANTIC_CLUSTER_MAX_SUGGESTIONS_PER_ASSET) || 8));
30
24
  const CLUSTERING_CONCURRENCY = Math.max(1, Math.min(8, Number(process.env.SEMANTIC_CLUSTER_CONCURRENCY) || 2));
@@ -72,13 +66,13 @@ const fallbackSlugFromSuggestion = (suggestionText) => {
72
66
  };
73
67
 
74
68
  const hashSuggestion = (normalizedSuggestion) =>
75
- createHash('sha256').update(String(normalizedSuggestion || ''), 'utf8').digest('hex');
69
+ createHash('sha256')
70
+ .update(String(normalizedSuggestion || ''), 'utf8')
71
+ .digest('hex');
76
72
 
77
73
  const serializeEmbedding = (embedding = []) => {
78
74
  const vector = Array.isArray(embedding) ? embedding : [];
79
- const clean = vector
80
- .map((value) => Number(value))
81
- .filter((value) => Number.isFinite(value));
75
+ const clean = vector.map((value) => Number(value)).filter((value) => Number.isFinite(value));
82
76
  if (!clean.length) return { dim: 0, buffer: Buffer.alloc(0) };
83
77
  const buffer = Buffer.allocUnsafe(clean.length * 4);
84
78
  for (let index = 0; index < clean.length; index += 1) {
@@ -157,14 +151,7 @@ const getSuggestionCacheRow = async (normalizedSuggestion) => {
157
151
  };
158
152
  };
159
153
 
160
- const upsertSuggestionCacheRow = async ({
161
- suggestionText,
162
- normalizedText,
163
- semanticClusterId,
164
- canonicalSlug,
165
- embedding = [],
166
- similarity = null,
167
- }) => {
154
+ const upsertSuggestionCacheRow = async ({ suggestionText, normalizedText, semanticClusterId, canonicalSlug, embedding = [], similarity = null }) => {
168
155
  const normalized = normalizeSuggestion(normalizedText || suggestionText);
169
156
  if (!normalized || !semanticClusterId) return false;
170
157
  const suggestionHash = hashSuggestion(normalized);
@@ -184,16 +171,7 @@ const upsertSuggestionCacheRow = async ({
184
171
  embedding = VALUES(embedding),
185
172
  last_similarity = VALUES(last_similarity),
186
173
  updated_at = CURRENT_TIMESTAMP`,
187
- [
188
- suggestionHash,
189
- String(suggestionText || normalized).slice(0, 512),
190
- normalized,
191
- semanticClusterId,
192
- canonicalSlug || null,
193
- dim,
194
- buffer,
195
- similarity !== null && Number.isFinite(Number(similarity)) ? Number(Number(similarity).toFixed(6)) : null,
196
- ],
174
+ [suggestionHash, String(suggestionText || normalized).slice(0, 512), normalized, semanticClusterId, canonicalSlug || null, dim, buffer, similarity !== null && Number.isFinite(Number(similarity)) ? Number(Number(similarity).toFixed(6)) : null],
197
175
  );
198
176
  return true;
199
177
  };
@@ -277,9 +255,7 @@ const generateEmbedding = async (text) => {
277
255
  });
278
256
  const vector = response?.data?.[0]?.embedding;
279
257
  if (!Array.isArray(vector) || !vector.length) return null;
280
- const clean = vector
281
- .map((value) => Number(value))
282
- .filter((value) => Number.isFinite(value));
258
+ const clean = vector.map((value) => Number(value)).filter((value) => Number.isFinite(value));
283
259
  return clean.length ? clean : null;
284
260
  };
285
261
 
@@ -540,19 +516,16 @@ const drainSemanticClusterQueue = async () => {
540
516
  }
541
517
  };
542
518
 
543
- export const enqueueSemanticClusterResolution = ({
544
- assetId,
545
- suggestions = [],
546
- fallbackText = '',
547
- force = false,
548
- } = {}) => {
519
+ export const enqueueSemanticClusterResolution = ({ assetId, suggestions = [], fallbackText = '', force = false } = {}) => {
549
520
  const normalizedAssetId = String(assetId || '').trim();
550
521
  if (!normalizedAssetId || !ENABLE_SEMANTIC_CLUSTERING) return false;
551
522
 
552
523
  pendingTasksByAssetId.set(normalizedAssetId, {
553
524
  assetId: normalizedAssetId,
554
525
  suggestions: sanitizeSuggestions(suggestions),
555
- fallbackText: String(fallbackText || '').trim().slice(0, 255),
526
+ fallbackText: String(fallbackText || '')
527
+ .trim()
528
+ .slice(0, 255),
556
529
  force: Boolean(force),
557
530
  });
558
531
  scheduleQueueDrain();
@@ -71,10 +71,7 @@ const normalizeClassificationRow = (row) => {
71
71
  llm_style_traits: Array.isArray(llmStyleTraits) ? llmStyleTraits : [],
72
72
  llm_emotions: Array.isArray(llmEmotions) ? llmEmotions : [],
73
73
  llm_pack_suggestions: Array.isArray(llmPackSuggestions) ? llmPackSuggestions : [],
74
- semantic_cluster_id:
75
- row.semantic_cluster_id !== null && row.semantic_cluster_id !== undefined
76
- ? Number(row.semantic_cluster_id)
77
- : null,
74
+ semantic_cluster_id: row.semantic_cluster_id !== null && row.semantic_cluster_id !== undefined ? Number(row.semantic_cluster_id) : null,
78
75
  semantic_cluster_slug: row.semantic_cluster_slug || null,
79
76
  classified_at: row.classified_at,
80
77
  updated_at: row.updated_at,
@@ -82,11 +79,7 @@ const normalizeClassificationRow = (row) => {
82
79
  };
83
80
 
84
81
  export async function findStickerClassificationByAssetId(assetId, connection = null) {
85
- const rows = await executeQuery(
86
- `SELECT * FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} WHERE asset_id = ? LIMIT 1`,
87
- [assetId],
88
- connection,
89
- );
82
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} WHERE asset_id = ? LIMIT 1`, [assetId], connection);
90
83
 
91
84
  return normalizeClassificationRow(rows?.[0] || null);
92
85
  }
@@ -98,11 +91,7 @@ export async function listStickerClassificationsByAssetIds(assetIds, connection
98
91
  if (!uniqueIds.length) return [];
99
92
 
100
93
  const placeholders = uniqueIds.map(() => '?').join(', ');
101
- const rows = await executeQuery(
102
- `SELECT * FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} WHERE asset_id IN (${placeholders})`,
103
- uniqueIds,
104
- connection,
105
- );
94
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET_CLASSIFICATION} WHERE asset_id IN (${placeholders})`, uniqueIds, connection);
106
95
 
107
96
  const normalized = rows.map((row) => normalizeClassificationRow(row));
108
97
  const byAssetId = new Map(normalized.map((entry) => [entry.asset_id, entry]));
@@ -138,30 +127,7 @@ export async function upsertStickerAssetClassification(payload, connection = nul
138
127
  similar_images = VALUES(similar_images),
139
128
  classified_at = CURRENT_TIMESTAMP,
140
129
  updated_at = CURRENT_TIMESTAMP`,
141
- [
142
- payload.asset_id,
143
- payload.provider || 'clip',
144
- payload.model_name || null,
145
- payload.classification_version || 'v1',
146
- payload.category || null,
147
- payload.confidence ?? null,
148
- payload.entropy ?? null,
149
- payload.confidence_margin ?? null,
150
- payload.nsfw_score ?? null,
151
- payload.is_nsfw ? 1 : 0,
152
- payload.all_scores ? JSON.stringify(payload.all_scores) : JSON.stringify({}),
153
- payload.top_labels ? JSON.stringify(payload.top_labels) : JSON.stringify([]),
154
- payload.affinity_weight ?? null,
155
- payload.image_hash || null,
156
- payload.ambiguous ? 1 : 0,
157
- payload.llm_subtags ? JSON.stringify(payload.llm_subtags) : JSON.stringify([]),
158
- payload.llm_style_traits ? JSON.stringify(payload.llm_style_traits) : JSON.stringify([]),
159
- payload.llm_emotions ? JSON.stringify(payload.llm_emotions) : JSON.stringify([]),
160
- payload.llm_pack_suggestions ? JSON.stringify(payload.llm_pack_suggestions) : JSON.stringify([]),
161
- payload.semantic_cluster_id ?? null,
162
- payload.semantic_cluster_slug || null,
163
- payload.similar_images ? JSON.stringify(payload.similar_images) : JSON.stringify([]),
164
- ],
130
+ [payload.asset_id, payload.provider || 'clip', payload.model_name || null, payload.classification_version || 'v1', payload.category || null, payload.confidence ?? null, payload.entropy ?? null, payload.confidence_margin ?? null, payload.nsfw_score ?? null, payload.is_nsfw ? 1 : 0, payload.all_scores ? JSON.stringify(payload.all_scores) : JSON.stringify({}), payload.top_labels ? JSON.stringify(payload.top_labels) : JSON.stringify([]), payload.affinity_weight ?? null, payload.image_hash || null, payload.ambiguous ? 1 : 0, payload.llm_subtags ? JSON.stringify(payload.llm_subtags) : JSON.stringify([]), payload.llm_style_traits ? JSON.stringify(payload.llm_style_traits) : JSON.stringify([]), payload.llm_emotions ? JSON.stringify(payload.llm_emotions) : JSON.stringify([]), payload.llm_pack_suggestions ? JSON.stringify(payload.llm_pack_suggestions) : JSON.stringify([]), payload.semantic_cluster_id ?? null, payload.semantic_cluster_slug || null, payload.similar_images ? JSON.stringify(payload.similar_images) : JSON.stringify([])],
165
131
  connection,
166
132
  );
167
133
 
@@ -187,22 +153,14 @@ export async function upsertStickerAssetClassification(payload, connection = nul
187
153
  return findStickerClassificationByAssetId(payload.asset_id, connection);
188
154
  }
189
155
 
190
- export async function updateStickerClassificationSemanticCluster(
191
- assetId,
192
- { semanticClusterId = null, semanticClusterSlug = null } = {},
193
- connection = null,
194
- ) {
156
+ export async function updateStickerClassificationSemanticCluster(assetId, { semanticClusterId = null, semanticClusterSlug = null } = {}, connection = null) {
195
157
  if (!assetId) return null;
196
158
 
197
159
  await executeQuery(
198
160
  `UPDATE ${TABLES.STICKER_ASSET_CLASSIFICATION}
199
161
  SET semantic_cluster_id = ?, semantic_cluster_slug = ?, updated_at = CURRENT_TIMESTAMP
200
162
  WHERE asset_id = ?`,
201
- [
202
- semanticClusterId ?? null,
203
- semanticClusterSlug || null,
204
- assetId,
205
- ],
163
+ [semanticClusterId ?? null, semanticClusterSlug || null, assetId],
206
164
  connection,
207
165
  );
208
166
 
@@ -211,9 +169,15 @@ export async function updateStickerClassificationSemanticCluster(
211
169
 
212
170
  export async function listClipImageEmbeddingsByImageHashes(imageHashes, connection = null) {
213
171
  const uniqueHashes = Array.from(
214
- new Set((Array.isArray(imageHashes) ? imageHashes : [])
215
- .map((value) => String(value || '').trim().toLowerCase())
216
- .filter((value) => value.length === 64)),
172
+ new Set(
173
+ (Array.isArray(imageHashes) ? imageHashes : [])
174
+ .map((value) =>
175
+ String(value || '')
176
+ .trim()
177
+ .toLowerCase(),
178
+ )
179
+ .filter((value) => value.length === 64),
180
+ ),
217
181
  );
218
182
  if (!uniqueHashes.length) return [];
219
183
 
@@ -249,10 +213,7 @@ const clampInt = (value, fallback, min, max) => {
249
213
  return Math.max(min, Math.min(max, Math.floor(numeric)));
250
214
  };
251
215
 
252
- export async function listAssetsForModelUpgradeReprocess(
253
- { currentVersion, limit = 150, offset = 0 } = {},
254
- connection = null,
255
- ) {
216
+ export async function listAssetsForModelUpgradeReprocess({ currentVersion, limit = 150, offset = 0 } = {}, connection = null) {
256
217
  const normalizedVersion = String(currentVersion || '').trim();
257
218
  if (!normalizedVersion) return [];
258
219
 
@@ -276,10 +237,7 @@ export async function listAssetsForModelUpgradeReprocess(
276
237
  return rows.map((row) => row.asset_id).filter(Boolean);
277
238
  }
278
239
 
279
- export async function listAssetsForLowConfidenceReprocess(
280
- { confidenceThreshold = 0.65, staleHours = 48, limit = 150, offset = 0 } = {},
281
- connection = null,
282
- ) {
240
+ export async function listAssetsForLowConfidenceReprocess({ confidenceThreshold = 0.65, staleHours = 48, limit = 150, offset = 0 } = {}, connection = null) {
283
241
  const threshold = Number(confidenceThreshold);
284
242
  if (!Number.isFinite(threshold)) return [];
285
243
 
@@ -307,10 +265,7 @@ export async function listAssetsForLowConfidenceReprocess(
307
265
  return rows.map((row) => row.asset_id).filter(Boolean);
308
266
  }
309
267
 
310
- export async function listAssetsForPrioritySignalBackfillReprocess(
311
- { limit = 200, offset = 0 } = {},
312
- connection = null,
313
- ) {
268
+ export async function listAssetsForPrioritySignalBackfillReprocess({ limit = 200, offset = 0 } = {}, connection = null) {
314
269
  const safeLimit = clampInt(limit, 200, 1, 2000);
315
270
  const safeOffset = clampInt(offset, 0, 0, 500000);
316
271
  const rows = await executeQuery(
@@ -358,23 +313,11 @@ export async function listAssetsForPrioritySignalBackfillReprocess(
358
313
  return rows.map((row) => row.asset_id).filter(Boolean);
359
314
  }
360
315
 
361
- export async function listStickerClassificationsForDeterministicReprocess(
362
- {
363
- limit = 250,
364
- cursorAssetId = '',
365
- entropyThreshold = 0.8,
366
- affinityThreshold = 0.3,
367
- } = {},
368
- connection = null,
369
- ) {
316
+ export async function listStickerClassificationsForDeterministicReprocess({ limit = 250, cursorAssetId = '', entropyThreshold = 0.8, affinityThreshold = 0.3 } = {}, connection = null) {
370
317
  const safeLimit = clampInt(limit, 250, 1, 2000);
371
318
  const normalizedCursor = String(cursorAssetId || '').trim();
372
- const normalizedEntropyThreshold = Number.isFinite(Number(entropyThreshold))
373
- ? Number(entropyThreshold)
374
- : 0.8;
375
- const normalizedAffinityThreshold = Number.isFinite(Number(affinityThreshold))
376
- ? Number(affinityThreshold)
377
- : 0.3;
319
+ const normalizedEntropyThreshold = Number.isFinite(Number(entropyThreshold)) ? Number(entropyThreshold) : 0.8;
320
+ const normalizedAffinityThreshold = Number.isFinite(Number(affinityThreshold)) ? Number(affinityThreshold) : 0.3;
378
321
 
379
322
  const params = [normalizedEntropyThreshold, normalizedAffinityThreshold];
380
323
  const cursorClause = normalizedCursor ? 'AND c.asset_id > ?' : '';
@@ -409,30 +352,17 @@ export async function listStickerClassificationsForDeterministicReprocess(
409
352
  return rows.map((row) => normalizeClassificationRow(row));
410
353
  }
411
354
 
412
- export async function updateStickerClassificationDeterministicSignals(
413
- assetId,
414
- { llmSubtags = [], affinityWeight = null, ambiguous = 0 } = {},
415
- connection = null,
416
- ) {
355
+ export async function updateStickerClassificationDeterministicSignals(assetId, { llmSubtags = [], affinityWeight = null, ambiguous = 0 } = {}, connection = null) {
417
356
  if (!assetId) return null;
418
- const normalizedAffinityWeight = Number.isFinite(Number(affinityWeight))
419
- ? Math.max(0, Math.min(1, Number(affinityWeight)))
420
- : null;
357
+ const normalizedAffinityWeight = Number.isFinite(Number(affinityWeight)) ? Math.max(0, Math.min(1, Number(affinityWeight))) : null;
421
358
  const normalizedAmbiguous = ambiguous === 1 || ambiguous === true ? 1 : 0;
422
- const normalizedSubtags = Array.isArray(llmSubtags)
423
- ? llmSubtags.map((value) => String(value || '').trim()).filter(Boolean)
424
- : [];
359
+ const normalizedSubtags = Array.isArray(llmSubtags) ? llmSubtags.map((value) => String(value || '').trim()).filter(Boolean) : [];
425
360
 
426
361
  await executeQuery(
427
362
  `UPDATE ${TABLES.STICKER_ASSET_CLASSIFICATION}
428
363
  SET llm_subtags = ?, affinity_weight = ?, ambiguous = ?, updated_at = CURRENT_TIMESTAMP
429
364
  WHERE asset_id = ?`,
430
- [
431
- JSON.stringify(normalizedSubtags),
432
- normalizedAffinityWeight,
433
- normalizedAmbiguous,
434
- assetId,
435
- ],
365
+ [JSON.stringify(normalizedSubtags), normalizedAffinityWeight, normalizedAmbiguous, assetId],
436
366
  connection,
437
367
  );
438
368
 
@@ -41,11 +41,7 @@ const normalizeStickerAssetRow = (row) => {
41
41
  * @returns {Promise<object|null>} Asset encontrado.
42
42
  */
43
43
  export async function findStickerAssetBySha256(sha256, connection = null) {
44
- const rows = await executeQuery(
45
- `SELECT * FROM ${TABLES.STICKER_ASSET} WHERE sha256 = ? LIMIT 1`,
46
- [sha256],
47
- connection,
48
- );
44
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET} WHERE sha256 = ? LIMIT 1`, [sha256], connection);
49
45
  return normalizeStickerAssetRow(rows?.[0] || null);
50
46
  }
51
47
 
@@ -74,11 +70,7 @@ export async function findStickerAssetsByIds(ids, connection = null) {
74
70
  if (!uniqueIds.length) return [];
75
71
 
76
72
  const placeholders = uniqueIds.map(() => '?').join(', ');
77
- const rows = await executeQuery(
78
- `SELECT * FROM ${TABLES.STICKER_ASSET} WHERE id IN (${placeholders})`,
79
- uniqueIds,
80
- connection,
81
- );
73
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET} WHERE id IN (${placeholders})`, uniqueIds, connection);
82
74
 
83
75
  const normalized = rows.map((row) => normalizeStickerAssetRow(row));
84
76
  const byId = new Map(normalized.map((row) => [row.id, row]));
@@ -143,7 +135,10 @@ export async function listStickerAssetsWithoutPack({ search = '', limit = 120, o
143
135
  const safeLimit = Math.max(1, Math.min(500, Number(limit) || 120));
144
136
  const safeOffset = Math.max(0, Number(offset) || 0);
145
137
  const safeLimitWithSentinel = safeLimit + 1;
146
- const normalizedSearch = String(search || '').trim().toLowerCase().slice(0, 140);
138
+ const normalizedSearch = String(search || '')
139
+ .trim()
140
+ .toLowerCase()
141
+ .slice(0, 140);
147
142
 
148
143
  const whereClauses = ['i.sticker_id IS NULL'];
149
144
  const params = [];
@@ -198,7 +193,10 @@ export async function listClassifiedStickerAssetsWithoutPack({ search = '', limi
198
193
  const safeLimit = Math.max(1, Math.min(500, Number(limit) || 120));
199
194
  const safeOffset = Math.max(0, Number(offset) || 0);
200
195
  const safeLimitWithSentinel = safeLimit + 1;
201
- const normalizedSearch = String(search || '').trim().toLowerCase().slice(0, 140);
196
+ const normalizedSearch = String(search || '')
197
+ .trim()
198
+ .toLowerCase()
199
+ .slice(0, 140);
202
200
 
203
201
  const whereClauses = ['i.sticker_id IS NULL'];
204
202
  const params = [];
@@ -274,14 +272,7 @@ export async function countClassifiedStickerAssetsWithoutPack(connection = null)
274
272
  * }} [options]
275
273
  * @returns {Promise<{ assets: object[], hasMore: boolean, total: number }>}
276
274
  */
277
- export async function listClassifiedStickerAssetsForCuration({
278
- limit = 200,
279
- offset = 0,
280
- includePacked = true,
281
- includeUnpacked = true,
282
- onlyVersionMismatch = null,
283
- connection = null,
284
- } = {}) {
275
+ export async function listClassifiedStickerAssetsForCuration({ limit = 200, offset = 0, includePacked = true, includeUnpacked = true, onlyVersionMismatch = null, connection = null } = {}) {
285
276
  const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
286
277
  const safeOffset = Math.max(0, Number(offset) || 0);
287
278
  const safeLimitWithSentinel = safeLimit + 1;
@@ -346,17 +337,7 @@ export async function createStickerAsset(asset, connection = null) {
346
337
  `INSERT INTO ${TABLES.STICKER_ASSET}
347
338
  (id, owner_jid, sha256, mimetype, is_animated, width, height, size_bytes, storage_path)
348
339
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
349
- [
350
- asset.id,
351
- asset.owner_jid,
352
- asset.sha256,
353
- asset.mimetype,
354
- asset.is_animated ? 1 : 0,
355
- asset.width ?? null,
356
- asset.height ?? null,
357
- asset.size_bytes,
358
- asset.storage_path,
359
- ],
340
+ [asset.id, asset.owner_jid, asset.sha256, asset.mimetype, asset.is_animated ? 1 : 0, asset.width ?? null, asset.height ?? null, asset.size_bytes, asset.storage_path],
360
341
  connection,
361
342
  );
362
343
 
@@ -3,13 +3,17 @@ import { randomUUID } from 'node:crypto';
3
3
  import { executeQuery, TABLES } from '../../../database/index.js';
4
4
 
5
5
  const normalizeReason = (value) => {
6
- const normalized = String(value || '').trim().toUpperCase();
6
+ const normalized = String(value || '')
7
+ .trim()
8
+ .toUpperCase();
7
9
  if (['MODEL_UPGRADE', 'LOW_CONFIDENCE', 'TREND_SHIFT', 'NSFW_REVIEW'].includes(normalized)) return normalized;
8
10
  return null;
9
11
  };
10
12
 
11
13
  const normalizeStatus = (value) => {
12
- const normalized = String(value || '').trim().toLowerCase();
14
+ const normalized = String(value || '')
15
+ .trim()
16
+ .toLowerCase();
13
17
  if (['pending', 'processing', 'completed', 'failed'].includes(normalized)) return normalized;
14
18
  return null;
15
19
  };
@@ -40,10 +44,7 @@ const normalizeRow = (row) => {
40
44
  };
41
45
  };
42
46
 
43
- export async function enqueueStickerAssetReprocess(
44
- { assetId, reason, priority = 50, scheduledAt = null, maxAttempts = 5 },
45
- connection = null,
46
- ) {
47
+ export async function enqueueStickerAssetReprocess({ assetId, reason, priority = 50, scheduledAt = null, maxAttempts = 5 }, connection = null) {
47
48
  const normalizedReason = normalizeReason(reason);
48
49
  if (!assetId || !normalizedReason) return false;
49
50
 
@@ -80,9 +81,7 @@ export async function claimStickerAssetReprocessTask({ reasons = [], allowRetryF
80
81
  const normalizedReasons = Array.from(new Set((Array.isArray(reasons) ? reasons : []).map(normalizeReason).filter(Boolean)));
81
82
  const reasonFilter = normalizedReasons.length ? `AND reason IN (${normalizedReasons.map(() => '?').join(', ')})` : '';
82
83
 
83
- const statusClause = allowRetryFailed
84
- ? "(status = 'pending' OR (status = 'failed' AND attempts < max_attempts))"
85
- : "status = 'pending'";
84
+ const statusClause = allowRetryFailed ? "(status = 'pending' OR (status = 'failed' AND attempts < max_attempts))" : "status = 'pending'";
86
85
 
87
86
  await executeQuery(
88
87
  `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
@@ -137,17 +136,13 @@ export async function completeStickerAssetReprocessTask(taskId, connection = nul
137
136
  return true;
138
137
  }
139
138
 
140
- export async function failStickerAssetReprocessTask(
141
- taskId,
142
- {
143
- error = null,
144
- retryDelaySeconds = 0,
145
- } = {},
146
- connection = null,
147
- ) {
139
+ export async function failStickerAssetReprocessTask(taskId, { error = null, retryDelaySeconds = 0 } = {}, connection = null) {
148
140
  if (!taskId) return false;
149
141
  const safeDelay = clampInt(retryDelaySeconds, 0, 0, 86400 * 7);
150
- const message = String(error || '').trim().slice(0, 255) || null;
142
+ const message =
143
+ String(error || '')
144
+ .trim()
145
+ .slice(0, 255) || null;
151
146
 
152
147
  await executeQuery(
153
148
  `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}