@omnizap-system/omnizap 2.5.12

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 (425) hide show
  1. package/.clusterfuzzlite/Dockerfile +10 -0
  2. package/.env.example +907 -0
  3. package/.github/codeql/codeql-config.yml +10 -0
  4. package/.github/dependabot.yml +35 -0
  5. package/.github/workflows/ci.yml +73 -0
  6. package/.github/workflows/codeql.yml +106 -0
  7. package/.github/workflows/db-migration-check.yml +98 -0
  8. package/.github/workflows/dependency-review.yml +22 -0
  9. package/.github/workflows/deploy.yml +95 -0
  10. package/.github/workflows/release.yml +106 -0
  11. package/.github/workflows/security-attest-provenance.yml +51 -0
  12. package/.github/workflows/security-gitleaks.yml +34 -0
  13. package/.github/workflows/security-runner-hardening.yml +31 -0
  14. package/.github/workflows/security-scorecard.yml +44 -0
  15. package/.github/workflows/security-zap-baseline.yml +44 -0
  16. package/.github/workflows/security-zap-full-scan.yml +43 -0
  17. package/.github/workflows/security-zizmor.yml +36 -0
  18. package/.github/workflows/wiki-sync.yml +44 -0
  19. package/.gitleaks.toml +15 -0
  20. package/.prettierrc +34 -0
  21. package/CODE_OF_CONDUCT.md +114 -0
  22. package/LICENSE +56 -0
  23. package/README.md +110 -0
  24. package/SECURITY.md +110 -0
  25. package/app/config/index.js +4 -0
  26. package/app/configParts/adminIdentity.js +92 -0
  27. package/app/configParts/baileysConfig.js +1818 -0
  28. package/app/configParts/groupUtils.js +692 -0
  29. package/app/configParts/loggerConfig.js +394 -0
  30. package/app/configParts/messagePersistenceService.js +305 -0
  31. package/app/connection/baileysCompatibility.test.js +40 -0
  32. package/app/connection/baileysDbAuthState.js +344 -0
  33. package/app/connection/socketController.js +2243 -0
  34. package/app/controllers/messageController.js +7 -0
  35. package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
  36. package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
  37. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
  38. package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
  39. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
  40. package/app/controllers/messageProcessingPipeline.js +699 -0
  41. package/app/modules/adminModule/AGENT.md +4056 -0
  42. package/app/modules/adminModule/adminAiHelpService.js +56 -0
  43. package/app/modules/adminModule/adminConfigRuntime.js +177 -0
  44. package/app/modules/adminModule/commandConfig.json +7122 -0
  45. package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
  46. package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
  47. package/app/modules/adminModule/groupEventHandlers.js +399 -0
  48. package/app/modules/aiModule/AGENT.md +547 -0
  49. package/app/modules/aiModule/aiAiHelpService.js +14 -0
  50. package/app/modules/aiModule/aiConfigRuntime.js +135 -0
  51. package/app/modules/aiModule/catCommand.js +967 -0
  52. package/app/modules/aiModule/commandConfig.json +981 -0
  53. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  54. package/app/modules/gameModule/AGENT.md +196 -0
  55. package/app/modules/gameModule/commandConfig.json +366 -0
  56. package/app/modules/gameModule/diceCommand.js +42 -0
  57. package/app/modules/gameModule/gameAiHelpService.js +14 -0
  58. package/app/modules/gameModule/gameConfigRuntime.js +68 -0
  59. package/app/modules/menuModule/AGENT.md +205 -0
  60. package/app/modules/menuModule/commandConfig.json +366 -0
  61. package/app/modules/menuModule/common.js +316 -0
  62. package/app/modules/menuModule/menuAiHelpService.js +14 -0
  63. package/app/modules/menuModule/menuConfigRuntime.js +68 -0
  64. package/app/modules/menuModule/menus.js +66 -0
  65. package/app/modules/playModule/AGENT.md +321 -0
  66. package/app/modules/playModule/commandConfig.json +584 -0
  67. package/app/modules/playModule/playAiHelpService.js +14 -0
  68. package/app/modules/playModule/playCommand.js +1417 -0
  69. package/app/modules/playModule/playConfigRuntime.js +68 -0
  70. package/app/modules/quoteModule/AGENT.md +199 -0
  71. package/app/modules/quoteModule/commandConfig.json +366 -0
  72. package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
  73. package/app/modules/quoteModule/quoteCommand.js +842 -0
  74. package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
  75. package/app/modules/rpgPokemonModule/AGENT.md +229 -0
  76. package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
  77. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
  78. package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
  79. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
  80. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  81. package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
  82. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
  83. package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
  84. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  85. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  86. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  87. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  88. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
  89. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
  90. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  91. package/app/modules/statsModule/AGENT.md +320 -0
  92. package/app/modules/statsModule/commandConfig.json +540 -0
  93. package/app/modules/statsModule/globalRankingCommand.js +64 -0
  94. package/app/modules/statsModule/rankingCommand.js +41 -0
  95. package/app/modules/statsModule/rankingCommon.js +1305 -0
  96. package/app/modules/statsModule/statsAiHelpService.js +14 -0
  97. package/app/modules/statsModule/statsConfigRuntime.js +68 -0
  98. package/app/modules/stickerModule/AGENT.md +692 -0
  99. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  100. package/app/modules/stickerModule/commandConfig.json +1216 -0
  101. package/app/modules/stickerModule/convertToWebp.js +367 -0
  102. package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
  103. package/app/modules/stickerModule/stickerCommand.js +446 -0
  104. package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
  105. package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
  106. package/app/modules/stickerModule/stickerTextCommand.js +653 -0
  107. package/app/modules/stickerPackModule/AGENT.md +215 -0
  108. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  109. package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
  110. package/app/modules/stickerPackModule/commandConfig.json +387 -0
  111. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
  112. package/app/modules/stickerPackModule/domainEvents.js +52 -0
  113. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
  114. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
  115. package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
  116. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
  117. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  118. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
  119. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
  120. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
  121. package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
  122. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
  123. package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
  124. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
  125. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
  126. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  127. package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
  128. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
  129. package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
  130. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
  131. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  132. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
  133. package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
  134. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
  135. package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
  136. package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
  137. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
  138. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
  139. package/app/modules/stickerPackModule/stickerPackService.js +733 -0
  140. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
  141. package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
  142. package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
  143. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
  144. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
  145. package/app/modules/systemMetricsModule/AGENT.md +193 -0
  146. package/app/modules/systemMetricsModule/commandConfig.json +344 -0
  147. package/app/modules/systemMetricsModule/pingCommand.js +399 -0
  148. package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
  149. package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
  150. package/app/modules/tiktokModule/AGENT.md +196 -0
  151. package/app/modules/tiktokModule/commandConfig.json +366 -0
  152. package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
  153. package/app/modules/tiktokModule/tiktokCommand.js +716 -0
  154. package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
  155. package/app/modules/userModule/AGENT.md +200 -0
  156. package/app/modules/userModule/commandConfig.json +386 -0
  157. package/app/modules/userModule/userAiHelpService.js +14 -0
  158. package/app/modules/userModule/userCommand.js +1155 -0
  159. package/app/modules/userModule/userConfigRuntime.js +68 -0
  160. package/app/modules/waifuPicsModule/AGENT.md +431 -0
  161. package/app/modules/waifuPicsModule/commandConfig.json +780 -0
  162. package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
  163. package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
  164. package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
  165. package/app/observability/metrics.js +766 -0
  166. package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
  167. package/app/services/ai/aiLearningRepository.js +400 -0
  168. package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
  169. package/app/services/ai/commandConfigEnrichmentService.js +452 -0
  170. package/app/services/ai/commandConfigValidationService.js +443 -0
  171. package/app/services/ai/commandToolBuilderService.js +192 -0
  172. package/app/services/ai/conversationRouterService.js +516 -0
  173. package/app/services/ai/geminiService.js +115 -0
  174. package/app/services/ai/geminiService.test.js +87 -0
  175. package/app/services/ai/globalModuleAiHelpService.js +1412 -0
  176. package/app/services/ai/globalToolCallingService.js +203 -0
  177. package/app/services/ai/messageCommandExecutionService.js +391 -0
  178. package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
  179. package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
  180. package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
  181. package/app/services/ai/moduleToolExecutorService.js +464 -0
  182. package/app/services/ai/moduleToolRegistryService.js +178 -0
  183. package/app/services/ai/toolCandidateSelectorService.js +781 -0
  184. package/app/services/auth/googleWebLinkService.js +80 -0
  185. package/app/services/auth/whatsappLoginLinkService.js +230 -0
  186. package/app/services/external/pokeApiService.js +398 -0
  187. package/app/services/group/groupMetadataService.js +311 -0
  188. package/app/services/infra/dbWriteQueue.js +874 -0
  189. package/app/services/infra/featureFlagService.js +131 -0
  190. package/app/services/infra/queueUtils.js +55 -0
  191. package/app/services/messaging/captchaService.js +491 -0
  192. package/app/services/messaging/messagePersistenceService.js +1 -0
  193. package/app/services/messaging/newsBroadcastService.js +347 -0
  194. package/app/services/sticker/stickerFocusService.js +347 -0
  195. package/app/services/sticker/stickerFocusService.test.js +43 -0
  196. package/app/store/aiPromptStore.js +38 -0
  197. package/app/store/conversationSessionStore.js +131 -0
  198. package/app/store/groupConfigStore.js +58 -0
  199. package/app/store/premiumUserStore.js +54 -0
  200. package/app/utils/antiLink/antiLinkModule.js +700 -0
  201. package/app/utils/http/getImageBufferModule.js +18 -0
  202. package/app/utils/json/jsonSanitizer.js +113 -0
  203. package/app/utils/json/jsonSanitizer.test.js +40 -0
  204. package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
  205. package/app/workers/aiLearningWorker.js +605 -0
  206. package/app/workers/commandConfigEnrichmentWorker.js +242 -0
  207. package/database/index.js +2075 -0
  208. package/database/init.js +151 -0
  209. package/database/migrations/.gitkeep +0 -0
  210. package/database/migrations/20260307_d0_hardening_down.sql +64 -0
  211. package/database/migrations/20260307_d0_hardening_up.sql +79 -0
  212. package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
  213. package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
  214. package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
  215. package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
  216. package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
  217. package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
  218. package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
  219. package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
  220. package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
  221. package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
  222. package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
  223. package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
  224. package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
  225. package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
  226. package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
  227. package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
  228. package/database/schema.sql +1186 -0
  229. package/docker-compose.yml +104 -0
  230. package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
  231. package/docs/audits/stickerCatalogController-symbols.md +58 -0
  232. package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
  233. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
  234. package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
  235. package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
  236. package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
  237. package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
  238. package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
  239. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
  240. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
  241. package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
  242. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
  243. package/docs/seo/satellite-page-template.md +116 -0
  244. package/docs/seo/satellite-pages-phase1.json +364 -0
  245. package/docs/wiki/Home.md +120 -0
  246. package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
  247. package/docs/wiki/recent-changes-2026-03-08.md +47 -0
  248. package/ecosystem.prod.config.cjs +135 -0
  249. package/eslint.config.js +89 -0
  250. package/index.js +488 -0
  251. package/ml/clip_classifier/Dockerfile +18 -0
  252. package/ml/clip_classifier/README.md +118 -0
  253. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  254. package/ml/clip_classifier/classifier.py +654 -0
  255. package/ml/clip_classifier/embedding_store.py +481 -0
  256. package/ml/clip_classifier/env_loader.py +15 -0
  257. package/ml/clip_classifier/llm_label_expander.py +144 -0
  258. package/ml/clip_classifier/main.py +213 -0
  259. package/ml/clip_classifier/requirements.txt +10 -0
  260. package/ml/clip_classifier/similarity_engine.py +74 -0
  261. package/new-logo.png +0 -0
  262. package/observability/alert-rules.yml +60 -0
  263. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  264. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  265. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  266. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  267. package/observability/loki-config.yml +38 -0
  268. package/observability/mysql-setup.sql +46 -0
  269. package/observability/prometheus.yml +35 -0
  270. package/observability/promtail-config.yml +84 -0
  271. package/observability/sticker-catalog-slo.md +83 -0
  272. package/observability/sticker-scale-hardening-rollout.md +128 -0
  273. package/package.json +144 -0
  274. package/public/apple-touch-icon.png +0 -0
  275. package/public/assets/css/commands-react.input.css +71 -0
  276. package/public/assets/css/create-pack-react.input.css +31 -0
  277. package/public/assets/css/home-react.input.css +106 -0
  278. package/public/assets/css/login-react.input.css +58 -0
  279. package/public/assets/css/stickers-react.input.css +18 -0
  280. package/public/assets/css/terms-react.input.css +115 -0
  281. package/public/assets/css/user-react.input.css +57 -0
  282. package/public/assets/images/brand-icon-192.png +0 -0
  283. package/public/assets/images/brand-logo-128.webp +0 -0
  284. package/public/assets/images/hero-banner-1280.jpg +0 -0
  285. package/public/comandos/commands-catalog.json +4517 -0
  286. package/public/css/api-docs.css +161 -0
  287. package/public/css/stickers-admin.css +1288 -0
  288. package/public/css/styles.css +679 -0
  289. package/public/css/systemadm/admin.css +474 -0
  290. package/public/css/systemadm/base.css +73 -0
  291. package/public/css/systemadm/components.css +662 -0
  292. package/public/css/systemadm/layout.css +229 -0
  293. package/public/css/systemadm/tokens.css +56 -0
  294. package/public/favicon-16x16.png +0 -0
  295. package/public/favicon-32x32.png +0 -0
  296. package/public/favicon.ico +0 -0
  297. package/public/js/apps/apiDocsApp.js +235 -0
  298. package/public/js/apps/commandsReactApp.js +528 -0
  299. package/public/js/apps/createPackApp.js +1646 -0
  300. package/public/js/apps/homeReactApp.js +942 -0
  301. package/public/js/apps/loginReactApp.js +496 -0
  302. package/public/js/apps/stickersAdminApp.js +1753 -0
  303. package/public/js/apps/stickersApp.js +3797 -0
  304. package/public/js/apps/termsReactApp.js +528 -0
  305. package/public/js/apps/userApp.js +2540 -0
  306. package/public/js/apps/userProfile/actions.js +66 -0
  307. package/public/js/apps/userReactApp.js +547 -0
  308. package/public/js/catalog.js +950 -0
  309. package/public/pages/api-docs.html +40 -0
  310. package/public/pages/aup.html +158 -0
  311. package/public/pages/comandos.html +41 -0
  312. package/public/pages/dpa.html +227 -0
  313. package/public/pages/home.html +45 -0
  314. package/public/pages/licenca.html +182 -0
  315. package/public/pages/login.html +40 -0
  316. package/public/pages/notice-and-takedown.html +234 -0
  317. package/public/pages/politica-de-privacidade.html +251 -0
  318. package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
  319. package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
  320. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
  321. package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
  322. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
  323. package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
  324. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
  325. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
  326. package/public/pages/stickers-admin.html +31 -0
  327. package/public/pages/stickers-create.html +41 -0
  328. package/public/pages/stickers.html +45 -0
  329. package/public/pages/suboperadores.html +237 -0
  330. package/public/pages/termos-de-uso-texto-integral.html +241 -0
  331. package/public/pages/termos-de-uso.html +41 -0
  332. package/public/pages/user-password-reset.html +32 -0
  333. package/public/pages/user-systemadm.html +508 -0
  334. package/public/pages/user.html +39 -0
  335. package/public/robots.txt +9 -0
  336. package/public/site.webmanifest +24 -0
  337. package/public/sitemap.xml +98 -0
  338. package/schemas/command-config.schema.json +582 -0
  339. package/scripts/baileys-compat-smoke.mjs +12 -0
  340. package/scripts/cache-bust.mjs +142 -0
  341. package/scripts/deploy.sh +916 -0
  342. package/scripts/email-broadcast-terms-update.mjs +170 -0
  343. package/scripts/enrich-command-discovery-fields.mjs +286 -0
  344. package/scripts/generate-command-config-schema.mjs +273 -0
  345. package/scripts/generate-commands-catalog.mjs +308 -0
  346. package/scripts/generate-module-agents.mjs +631 -0
  347. package/scripts/generate-seo-satellite-pages.mjs +400 -0
  348. package/scripts/github-deploy-notify.mjs +174 -0
  349. package/scripts/github-release-notify.mjs +219 -0
  350. package/scripts/release.sh +599 -0
  351. package/scripts/run-codeql-local.sh +116 -0
  352. package/scripts/run-prettier-all.mjs +25 -0
  353. package/scripts/security-smoketest.mjs +581 -0
  354. package/scripts/sticker-catalog-loadtest.mjs +210 -0
  355. package/scripts/sticker-worker-task.mjs +119 -0
  356. package/scripts/sync-readme-snapshot.mjs +133 -0
  357. package/scripts/validate-command-config-schema.mjs +130 -0
  358. package/scripts/validate-command-configs.mjs +15 -0
  359. package/scripts/wiki-sync.sh +191 -0
  360. package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
  361. package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
  362. package/server/auth/jwt/webJwtService.js +147 -0
  363. package/server/auth/stickerCatalogAuthContext.js +165 -0
  364. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
  365. package/server/auth/userPassword/index.js +14 -0
  366. package/server/auth/userPassword/userPasswordAuthService.js +422 -0
  367. package/server/auth/userPassword/userPasswordCrypto.js +199 -0
  368. package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
  369. package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
  370. package/server/auth/validation/authSchemas.js +236 -0
  371. package/server/auth/webAccount/webAccountHandlers.js +1434 -0
  372. package/server/controllers/admin/adminBanService.js +138 -0
  373. package/server/controllers/admin/adminPanelHandlers.js +2083 -0
  374. package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
  375. package/server/controllers/admin/systemAdminController.js +201 -0
  376. package/server/controllers/email/emailAutomationController.js +239 -0
  377. package/server/controllers/metricsController.js +21 -0
  378. package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
  379. package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
  380. package/server/controllers/sticker/stickerCatalogController.js +4700 -0
  381. package/server/controllers/system/contactController.js +115 -0
  382. package/server/controllers/system/githubController.js +137 -0
  383. package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
  384. package/server/controllers/system/storageController.js +154 -0
  385. package/server/controllers/system/systemController.js +135 -0
  386. package/server/controllers/system/systemMetricsController.js +156 -0
  387. package/server/controllers/system/visitController.js +90 -0
  388. package/server/controllers/userController.js +145 -0
  389. package/server/email/emailAutomationRuntime.js +225 -0
  390. package/server/email/emailAutomationService.js +125 -0
  391. package/server/email/emailOutboxRepository.js +282 -0
  392. package/server/email/emailTemplateService.js +480 -0
  393. package/server/email/emailTransportService.js +156 -0
  394. package/server/http/clientIp.js +95 -0
  395. package/server/http/httpRequestUtils.js +262 -0
  396. package/server/http/httpRequestUtils.test.js +80 -0
  397. package/server/http/httpServer.js +180 -0
  398. package/server/http/requestContext.js +20 -0
  399. package/server/http/siteRoutingUtils.js +87 -0
  400. package/server/index.js +1 -0
  401. package/server/middleware/cachePolicy.js +26 -0
  402. package/server/middleware/cachePolicyHelpers.js +1 -0
  403. package/server/middleware/endpointRateLimit.js +181 -0
  404. package/server/middleware/rateLimit.js +70 -0
  405. package/server/middleware/requireAdminAuth.js +48 -0
  406. package/server/middleware/securityHeaders.js +97 -0
  407. package/server/routes/admin/systemAdminRouter.js +64 -0
  408. package/server/routes/email/emailAutomationRouter.js +46 -0
  409. package/server/routes/health/healthRouter.js +41 -0
  410. package/server/routes/indexRouter.js +234 -0
  411. package/server/routes/metrics/metricsRouter.js +58 -0
  412. package/server/routes/static/staticPageRouter.js +134 -0
  413. package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
  414. package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
  415. package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
  416. package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
  417. package/server/routes/sticker/catalogRouter.js +77 -0
  418. package/server/routes/sticker/stickerApiRouter.js +84 -0
  419. package/server/routes/sticker/stickerDataRouter.js +145 -0
  420. package/server/routes/sticker/stickerSiteRouter.js +43 -0
  421. package/server/routes/user/userApiPaths.js +66 -0
  422. package/server/routes/user/userRouter.js +65 -0
  423. package/server/utils/safePath.js +26 -0
  424. package/utils/logger/loggerModule.js +35 -0
  425. package/vite.config.mjs +38 -0
@@ -0,0 +1,654 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import io
5
+ import json
6
+ import math
7
+ import os
8
+ import threading
9
+ from dataclasses import dataclass
10
+ from typing import Any, Iterable
11
+
12
+ import numpy as np
13
+ import open_clip
14
+ import torch
15
+ from PIL import Image, UnidentifiedImageError
16
+
17
+ from adaptive_scoring import apply_adaptive_scores, confidence_margin, top_k_items
18
+ from embedding_store import EmbeddingStore
19
+ from env_loader import load_project_env
20
+ from llm_label_expander import expand_labels_with_llm
21
+ from similarity_engine import cosine_similarity_matrix as cosine_similarity_matrix_np
22
+ from similarity_engine import find_similar_images
23
+
24
+ load_project_env()
25
+
26
+ BUILTIN_DEFAULT_LABELS = [
27
+ "anime illustration",
28
+ "manga panel",
29
+ "cartoon",
30
+ "comic art",
31
+ "chibi character",
32
+ "3d render",
33
+ "pixel art",
34
+ "vector illustration",
35
+ "line art drawing",
36
+ "watercolor painting",
37
+ "oil painting",
38
+ "real life photo",
39
+ "portrait photo",
40
+ "selfie photo",
41
+ "group photo",
42
+ "close-up face photo",
43
+ "landscape photo",
44
+ "cityscape photo",
45
+ "street photography",
46
+ "night photo",
47
+ "indoor photo",
48
+ "outdoor photo",
49
+ "nature photo",
50
+ "animal photo",
51
+ "pet photo",
52
+ "food photo",
53
+ "product photo",
54
+ "car photo",
55
+ "motorcycle photo",
56
+ "document screenshot",
57
+ "website screenshot",
58
+ "mobile app screenshot",
59
+ "desktop app screenshot",
60
+ "chat screenshot",
61
+ "video game screenshot",
62
+ "fps game screenshot",
63
+ "rpg game screenshot",
64
+ "moba game screenshot",
65
+ "racing game screenshot",
66
+ "sports game screenshot",
67
+ "stream overlay screenshot",
68
+ "meme image",
69
+ "reaction meme",
70
+ "shitpost meme",
71
+ "motivational quote image",
72
+ "text-only image",
73
+ "poster design",
74
+ "banner design",
75
+ "logo design",
76
+ "brand identity image",
77
+ "infographic",
78
+ "presentation slide",
79
+ "advertisement image",
80
+ "flyer design",
81
+ "event poster",
82
+ "album cover",
83
+ "book cover",
84
+ "movie poster",
85
+ "anime wallpaper",
86
+ "gaming wallpaper",
87
+ "abstract wallpaper",
88
+ "minimal wallpaper",
89
+ "tech wallpaper",
90
+ "sticker style image",
91
+ "emoji style image",
92
+ "telegram sticker style",
93
+ "whatsapp sticker style",
94
+ "cute style image",
95
+ "kawaii style image",
96
+ "dark aesthetic image",
97
+ "cyberpunk style image",
98
+ "fantasy art",
99
+ "sci-fi art",
100
+ "horror art",
101
+ "gothic style image",
102
+ "retro style image",
103
+ "vaporwave style image",
104
+ "glitch art",
105
+ "low quality compressed image",
106
+ "blurry image",
107
+ "watermarked image",
108
+ "collage image",
109
+ "photo with overlaid text",
110
+ "handwritten note photo",
111
+ "whiteboard photo",
112
+ "code screenshot",
113
+ "terminal screenshot",
114
+ "dashboard screenshot",
115
+ "chart screenshot",
116
+ "ui mockup",
117
+ "wireframe design",
118
+ "architecture photo",
119
+ "interior design photo",
120
+ "fashion photo",
121
+ "beauty photo",
122
+ "wedding photo",
123
+ "party photo",
124
+ "sports photo",
125
+ "gym photo",
126
+ "travel photo",
127
+ "beach photo",
128
+ "mountain photo",
129
+ "forest photo",
130
+ "rainy weather photo",
131
+ "sunset photo",
132
+ "space themed image",
133
+ "medical image",
134
+ "educational image",
135
+ "news image",
136
+ "political image",
137
+ "religious image",
138
+ "family-friendly content",
139
+ "violent content",
140
+ "weapon content",
141
+ "gore content",
142
+ "drug-related content",
143
+ "alcohol-related content",
144
+ "smoking-related content",
145
+ "suggestive content",
146
+ "nsfw content",
147
+ "adult explicit content",
148
+ ]
149
+
150
+
151
+ def _parse_env_bool(value: str | None, fallback: bool) -> bool:
152
+ if value is None:
153
+ return fallback
154
+ normalized = str(value).strip().lower()
155
+ if normalized in {"1", "true", "yes", "y", "on"}:
156
+ return True
157
+ if normalized in {"0", "false", "no", "n", "off"}:
158
+ return False
159
+ return fallback
160
+
161
+
162
+ NSFW_THRESHOLD = float(os.getenv("NSFW_THRESHOLD", "0.6"))
163
+ CLIP_MODEL_NAME = os.getenv("CLIP_MODEL_NAME", "MobileCLIP-S1")
164
+ CLIP_MODEL_PRETRAINED = os.getenv("CLIP_MODEL_PRETRAINED", "datacompdr")
165
+ MAX_LABELS = max(5, int(os.getenv("CLIP_MAX_LABELS", "256")))
166
+ CLIP_TOP_K = max(1, min(20, int(os.getenv("CLIP_TOP_K", "5") or 5)))
167
+ ENABLE_EMBEDDING_CACHE = _parse_env_bool(os.getenv("ENABLE_EMBEDDING_CACHE"), True)
168
+ ENABLE_CLUSTERING = _parse_env_bool(os.getenv("ENABLE_CLUSTERING"), True)
169
+ ENABLE_ADAPTIVE_SCORING = _parse_env_bool(os.getenv("ENABLE_ADAPTIVE_SCORING"), True)
170
+ ENABLE_LLM_LABEL_EXPANSION = _parse_env_bool(os.getenv("ENABLE_LLM_LABEL_EXPANSION"), True)
171
+ ADAPTIVE_ALPHA = float(os.getenv("ADAPTIVE_ALPHA", "0.4") or 0.4)
172
+ ENTROPY_THRESHOLD = float(os.getenv("ENTROPY_THRESHOLD", "2.5") or 2.5)
173
+ ENTROPY_NORMALIZED_THRESHOLD = float(os.getenv("ENTROPY_NORMALIZED_THRESHOLD", "0.76") or 0.76)
174
+ AFFINITY_WEIGHT_CAP = max(0.0, min(1.0, float(os.getenv("AFFINITY_WEIGHT_CAP", "0.85") or 0.85)))
175
+ ENABLE_AFFINITY_LOG_SCALING = _parse_env_bool(os.getenv("ENABLE_AFFINITY_LOG_SCALING"), True)
176
+ AFFINITY_LOG_SCALE = max(0.1, float(os.getenv("AFFINITY_LOG_SCALE", "4.0") or 4.0))
177
+ ENABLE_LLM_EXPANSION_GATING = _parse_env_bool(os.getenv("ENABLE_LLM_EXPANSION_GATING"), True)
178
+ LLM_EXPANSION_MAX_ENTROPY_NORMALIZED = max(
179
+ 0.0,
180
+ min(1.0, float(os.getenv("LLM_EXPANSION_MAX_ENTROPY_NORMALIZED", "0.72") or 0.72)),
181
+ )
182
+ LLM_EXPANSION_MIN_MARGIN = max(0.0, min(1.0, float(os.getenv("LLM_EXPANSION_MIN_MARGIN", "0.08") or 0.08)))
183
+ SIMILARITY_THRESHOLD_DEFAULT = float(os.getenv("SIMILARITY_THRESHOLD", "0.85") or 0.85)
184
+ SIMILARITY_LIMIT_DEFAULT = max(1, min(100, int(os.getenv("SIMILARITY_LIMIT", "25") or 25)))
185
+ SIMILARITY_SCAN_LIMIT = max(100, min(20000, int(os.getenv("SIMILARITY_SCAN_LIMIT", "3000") or 3000)))
186
+
187
+
188
+ def _resolve_device() -> str:
189
+ forced = os.getenv("CLIP_DEVICE", "").strip().lower()
190
+ if forced in {"cpu", "cuda"}:
191
+ if forced == "cuda" and not torch.cuda.is_available():
192
+ return "cpu"
193
+ return forced
194
+ return "cuda" if torch.cuda.is_available() else "cpu"
195
+
196
+
197
+ def _load_labels_from_raw(raw_value: str) -> list[str]:
198
+ raw = str(raw_value or "").strip()
199
+ if not raw:
200
+ return []
201
+
202
+ if raw.startswith("["):
203
+ try:
204
+ parsed = json.loads(raw)
205
+ except json.JSONDecodeError:
206
+ parsed = None
207
+ if isinstance(parsed, list):
208
+ return [str(item).strip() for item in parsed if str(item).strip()]
209
+
210
+ if "\n" in raw:
211
+ return [line.strip() for line in raw.splitlines() if line.strip()]
212
+
213
+ return [item.strip() for item in raw.split(",") if item.strip()]
214
+
215
+
216
+ def _load_default_labels() -> list[str]:
217
+ env_json = os.getenv("CLIP_DEFAULT_LABELS_JSON", "").strip()
218
+ if env_json:
219
+ labels = _load_labels_from_raw(env_json)
220
+ if labels:
221
+ return labels
222
+
223
+ env_path = os.getenv("CLIP_DEFAULT_LABELS_PATH", "").strip()
224
+ if env_path:
225
+ try:
226
+ with open(env_path, "r", encoding="utf-8") as file:
227
+ labels = _load_labels_from_raw(file.read())
228
+ if labels:
229
+ return labels
230
+ except OSError:
231
+ pass
232
+
233
+ return list(BUILTIN_DEFAULT_LABELS)
234
+
235
+
236
+ def _normalize_labels(labels: Iterable[str] | None) -> list[str]:
237
+ if labels is None:
238
+ labels = DEFAULT_LABELS
239
+
240
+ clean: list[str] = []
241
+ seen: set[str] = set()
242
+ for label in labels:
243
+ normalized = str(label).strip()
244
+ if not normalized:
245
+ continue
246
+ key = normalized.lower()
247
+ if key in seen:
248
+ continue
249
+ clean.append(normalized)
250
+ seen.add(key)
251
+
252
+ if not clean:
253
+ raise ValueError("labels não pode ser vazio")
254
+ if len(clean) > MAX_LABELS:
255
+ clean = clean[:MAX_LABELS]
256
+ return clean
257
+
258
+
259
+ def _pick_nsfw_label(labels: list[str]) -> str | None:
260
+ keywords = ("nsfw", "adult", "explicit", "porn", "sexual")
261
+ for label in labels:
262
+ if any(keyword in label.lower() for keyword in keywords):
263
+ return label
264
+ return None
265
+
266
+
267
+ def _softmax(logits: np.ndarray) -> np.ndarray:
268
+ stable = logits - np.max(logits)
269
+ exp = np.exp(stable)
270
+ return exp / np.clip(exp.sum(), 1e-12, None)
271
+
272
+
273
+ def _entropy(probabilities: np.ndarray) -> float:
274
+ p = np.clip(probabilities.astype(np.float64), 1e-12, 1.0)
275
+ return float(-(p * np.log(p)).sum())
276
+
277
+
278
+ def _normalize_entropy(entropy: float, k: int) -> float:
279
+ safe_k = max(1, int(k or 1))
280
+ if safe_k <= 1:
281
+ return 0.0
282
+ max_entropy = math.log(float(safe_k))
283
+ if max_entropy <= 1e-12:
284
+ return 0.0
285
+ return float(np.clip(float(entropy) / max_entropy, 0.0, 1.0))
286
+
287
+
288
+ def _normalize_affinity_weight(raw_affinity: float) -> float:
289
+ safe = float(np.clip(float(raw_affinity or 0.0), 0.0, 1.0))
290
+ capped = min(safe, AFFINITY_WEIGHT_CAP)
291
+ if not ENABLE_AFFINITY_LOG_SCALING:
292
+ return capped
293
+ return float(math.log1p(capped * AFFINITY_LOG_SCALE) / math.log1p(AFFINITY_LOG_SCALE))
294
+
295
+
296
+ def _round_map(values: dict[str, float], precision: int = 6) -> dict[str, float]:
297
+ return {key: round(float(value), precision) for key, value in values.items()}
298
+
299
+
300
+ def _normalize_theme(value: str | None) -> str:
301
+ return str(value or "").strip().lower()[:120]
302
+
303
+
304
+ @dataclass(frozen=True)
305
+ class ClassifierRuntimeInfo:
306
+ model_name: str
307
+ device: str
308
+
309
+
310
+ class ClipClassifier:
311
+ def __init__(
312
+ self,
313
+ model_name: str = CLIP_MODEL_NAME,
314
+ pretrained: str = CLIP_MODEL_PRETRAINED,
315
+ device: str | None = None,
316
+ ) -> None:
317
+ self.device = device or _resolve_device()
318
+ self.model_name = model_name
319
+ self.pretrained = pretrained
320
+ self.model, _, self.preprocess = open_clip.create_model_and_transforms(
321
+ self.model_name,
322
+ pretrained=self.pretrained,
323
+ )
324
+ self.model = self.model.to(self.device)
325
+ self.model.eval()
326
+ self.tokenizer = open_clip.get_tokenizer(self.model_name)
327
+ self.embedding_store = EmbeddingStore()
328
+
329
+ self._label_embeddings_cache: dict[str, np.ndarray] = {}
330
+ self._label_lock = threading.Lock()
331
+
332
+ @property
333
+ def runtime_info(self) -> ClassifierRuntimeInfo:
334
+ return ClassifierRuntimeInfo(model_name=self.model_name, device=self.device)
335
+
336
+ @property
337
+ def logit_scale_value(self) -> float:
338
+ scale = getattr(self.model, "logit_scale", None)
339
+ if isinstance(scale, torch.Tensor):
340
+ return float(scale.exp().detach().cpu().item())
341
+ return 100.0
342
+
343
+ def _compute_image_embedding(self, image: Image.Image) -> np.ndarray:
344
+ image_tensor = self.preprocess(image.convert("RGB")).unsqueeze(0).to(self.device)
345
+ with torch.no_grad():
346
+ features = self.model.encode_image(image_tensor)
347
+ features = features / features.norm(dim=-1, keepdim=True).clamp(min=1e-12)
348
+ return features.squeeze(0).float().detach().cpu().numpy().astype(np.float32)
349
+
350
+ def _compute_label_embeddings(self, labels: list[str]) -> dict[str, np.ndarray]:
351
+ if not labels:
352
+ return {}
353
+
354
+ text_tokens = self.tokenizer(labels).to(self.device)
355
+ with torch.no_grad():
356
+ features = self.model.encode_text(text_tokens)
357
+ features = features / features.norm(dim=-1, keepdim=True).clamp(min=1e-12)
358
+
359
+ matrix = features.float().detach().cpu().numpy().astype(np.float32)
360
+ return {label: matrix[idx] for idx, label in enumerate(labels)}
361
+
362
+ def get_label_embeddings(self, labels: list[str]) -> dict[str, np.ndarray]:
363
+ clean_labels = _normalize_labels(labels)
364
+ out: dict[str, np.ndarray] = {}
365
+ missing: list[str] = []
366
+
367
+ with self._label_lock:
368
+ for label in clean_labels:
369
+ cached = self._label_embeddings_cache.get(label)
370
+ if cached is None:
371
+ missing.append(label)
372
+ else:
373
+ out[label] = cached
374
+
375
+ if missing and ENABLE_EMBEDDING_CACHE:
376
+ persisted = self.embedding_store.get_label_embeddings(self.model_name, missing)
377
+ for label, vector in persisted.items():
378
+ out[label] = vector
379
+ missing = [label for label in missing if label not in persisted]
380
+
381
+ if missing:
382
+ computed = self._compute_label_embeddings(missing)
383
+ out.update(computed)
384
+ if ENABLE_EMBEDDING_CACHE and computed:
385
+ self.embedding_store.save_label_embeddings(self.model_name, computed)
386
+
387
+ with self._label_lock:
388
+ self._label_embeddings_cache.update(out)
389
+
390
+ return {label: out[label] for label in clean_labels if label in out}
391
+
392
+ def get_image_embedding(
393
+ self,
394
+ image: Image.Image,
395
+ *,
396
+ image_hash: str | None = None,
397
+ asset_id: str | None = None,
398
+ ) -> np.ndarray:
399
+ if ENABLE_EMBEDDING_CACHE and image_hash:
400
+ cached = self.embedding_store.get_image_embedding(image_hash=image_hash, model_name=self.model_name)
401
+ if cached is not None and cached.size > 0:
402
+ return cached.astype(np.float32)
403
+
404
+ computed = self._compute_image_embedding(image)
405
+ if ENABLE_EMBEDDING_CACHE and image_hash:
406
+ self.embedding_store.save_image_embedding(
407
+ image_hash=image_hash,
408
+ model_name=self.model_name,
409
+ embedding=computed,
410
+ asset_id=asset_id,
411
+ )
412
+ return computed
413
+
414
+ def classify_pil(
415
+ self,
416
+ image: Image.Image,
417
+ labels: Iterable[str] | None = None,
418
+ nsfw_threshold: float = NSFW_THRESHOLD,
419
+ *,
420
+ image_hash: str | None = None,
421
+ asset_id: str | None = None,
422
+ theme: str | None = None,
423
+ similar_threshold: float | None = None,
424
+ similar_limit: int | None = None,
425
+ ) -> dict[str, Any]:
426
+ clean_labels = _normalize_labels(labels)
427
+ nsfw_label = _pick_nsfw_label(clean_labels)
428
+ normalized_theme = _normalize_theme(theme)
429
+
430
+ image_embedding = self.get_image_embedding(image=image, image_hash=image_hash, asset_id=asset_id)
431
+ label_embeddings = self.get_label_embeddings(clean_labels)
432
+
433
+ text_matrix = np.asarray([label_embeddings[label] for label in clean_labels], dtype=np.float32)
434
+ logits_vector = (image_embedding.reshape(1, -1) @ text_matrix.T).reshape(-1) * self.logit_scale_value
435
+
436
+ logits_by_label = {label: float(logits_vector[idx]) for idx, label in enumerate(clean_labels)}
437
+ probabilities = _softmax(logits_vector)
438
+ base_scores = {label: float(probabilities[idx]) for idx, label in enumerate(clean_labels)}
439
+
440
+ affinity_weight_raw = 0.0
441
+ affinity_weight = 0.0
442
+ effective_scores = base_scores
443
+ if ENABLE_ADAPTIVE_SCORING and image_hash and normalized_theme:
444
+ affinity_weight_raw = self.embedding_store.get_affinity_weight(image_hash=image_hash, theme=normalized_theme)
445
+ affinity_weight = _normalize_affinity_weight(affinity_weight_raw)
446
+ effective_scores = apply_adaptive_scores(
447
+ base_scores=base_scores,
448
+ affinity_weight=affinity_weight,
449
+ alpha=ADAPTIVE_ALPHA,
450
+ )
451
+
452
+ ordered = sorted(
453
+ clean_labels,
454
+ key=lambda label: (effective_scores.get(label, 0.0), logits_by_label.get(label, -math.inf)),
455
+ reverse=True,
456
+ )
457
+
458
+ top_k = min(CLIP_TOP_K, len(ordered))
459
+ top_labels_payload = []
460
+ for label in ordered[:top_k]:
461
+ top_labels_payload.append(
462
+ {
463
+ "label": label,
464
+ "score": round(float(effective_scores.get(label, 0.0)), 6),
465
+ "logit": round(float(logits_by_label.get(label, 0.0)), 6),
466
+ "clip_score": round(float(base_scores.get(label, 0.0)), 6),
467
+ }
468
+ )
469
+
470
+ top_items = top_k_items(effective_scores, top_k)
471
+ best_label = top_items[0][0] if top_items else ordered[0]
472
+ best_score = float(effective_scores.get(best_label, 0.0))
473
+
474
+ entropy = _entropy(np.asarray([effective_scores[label] for label in clean_labels], dtype=np.float64))
475
+ top_entropy_labels = ordered[:top_k]
476
+ top_entropy_scores = np.asarray([effective_scores[label] for label in top_entropy_labels], dtype=np.float64)
477
+ top_entropy_total = float(top_entropy_scores.sum())
478
+ if top_entropy_total > 1e-12:
479
+ top_entropy_scores = top_entropy_scores / top_entropy_total
480
+ entropy_top_k = _entropy(top_entropy_scores) if top_entropy_scores.size else 0.0
481
+ entropy_normalized = _normalize_entropy(entropy_top_k, len(top_entropy_labels))
482
+ margin = confidence_margin(top_items)
483
+ nsfw_score = float(effective_scores.get(nsfw_label, 0.0)) if nsfw_label else 0.0
484
+
485
+ ambiguous = (float(entropy) > float(ENTROPY_THRESHOLD)) or (
486
+ float(entropy_normalized) > float(ENTROPY_NORMALIZED_THRESHOLD)
487
+ )
488
+
489
+ llm_allowed = bool(ENABLE_LLM_LABEL_EXPANSION)
490
+ llm_gate_reason = "enabled"
491
+ if ENABLE_LLM_EXPANSION_GATING:
492
+ if entropy_normalized > LLM_EXPANSION_MAX_ENTROPY_NORMALIZED:
493
+ llm_allowed = False
494
+ llm_gate_reason = "entropy_gate"
495
+ elif margin < LLM_EXPANSION_MIN_MARGIN:
496
+ llm_allowed = False
497
+ llm_gate_reason = "margin_gate"
498
+
499
+ llm_expansion = expand_labels_with_llm(
500
+ top_labels=[entry["label"] for entry in top_labels_payload[:3]],
501
+ store=self.embedding_store,
502
+ enabled=llm_allowed,
503
+ )
504
+
505
+ similar_images = []
506
+ if ENABLE_CLUSTERING and ENABLE_EMBEDDING_CACHE and image_embedding.size > 0:
507
+ threshold = float(similar_threshold) if similar_threshold is not None else SIMILARITY_THRESHOLD_DEFAULT
508
+ limit = int(similar_limit) if similar_limit is not None else SIMILARITY_LIMIT_DEFAULT
509
+ similar_images = find_similar_images(
510
+ store=self.embedding_store,
511
+ image_embedding=image_embedding,
512
+ model_name=self.model_name,
513
+ threshold=threshold,
514
+ limit=limit,
515
+ scan_limit=SIMILARITY_SCAN_LIMIT,
516
+ source_image_hash=image_hash,
517
+ )
518
+
519
+ return {
520
+ "category": best_label,
521
+ "confidence": round(best_score, 6),
522
+ "all_scores": _round_map(effective_scores),
523
+ "raw_logits": _round_map(logits_by_label),
524
+ "top_labels": top_labels_payload,
525
+ "entropy": round(float(entropy), 6),
526
+ "entropy_top_k": round(float(entropy_top_k), 6),
527
+ "entropy_normalized": round(float(entropy_normalized), 6),
528
+ "confidence_margin": round(float(margin), 6),
529
+ "nsfw_score": round(nsfw_score, 6),
530
+ "is_nsfw": nsfw_score >= float(nsfw_threshold),
531
+ "ambiguous": ambiguous,
532
+ "affinity_weight": round(float(affinity_weight), 6),
533
+ "affinity_weight_raw": round(float(affinity_weight_raw), 6),
534
+ "llm_expansion_used": bool(llm_allowed),
535
+ "llm_expansion_gate_reason": llm_gate_reason,
536
+ "llm_expansion": llm_expansion,
537
+ "similar_images": similar_images,
538
+ "image_hash": image_hash,
539
+ "model_name": self.model_name,
540
+ }
541
+
542
+
543
+ DEFAULT_LABELS = _load_default_labels()
544
+
545
+
546
+ _service: ClipClassifier | None = None
547
+ _service_lock = threading.Lock()
548
+
549
+
550
+ def get_classifier() -> ClipClassifier:
551
+ global _service
552
+ if _service is not None:
553
+ return _service
554
+
555
+ with _service_lock:
556
+ if _service is None:
557
+ _service = ClipClassifier()
558
+ return _service
559
+
560
+
561
+ def get_image_embedding(image: Image.Image) -> np.ndarray:
562
+ return get_classifier().get_image_embedding(image)
563
+
564
+
565
+ def get_label_embeddings(labels: Iterable[str]) -> dict[str, list[float]]:
566
+ embeddings = get_classifier().get_label_embeddings(_normalize_labels(labels))
567
+ return {label: vector.astype(np.float32).tolist() for label, vector in embeddings.items()}
568
+
569
+
570
+ def cosine_similarity_matrix(a: np.ndarray, b: np.ndarray) -> np.ndarray:
571
+ return cosine_similarity_matrix_np(a, b)
572
+
573
+
574
+ def register_pack_feedback(
575
+ *,
576
+ image_hash: str,
577
+ theme: str,
578
+ accepted: bool,
579
+ asset_id: str | None = None,
580
+ ) -> None:
581
+ get_classifier().embedding_store.record_feedback(
582
+ image_hash=image_hash,
583
+ theme=_normalize_theme(theme),
584
+ accepted=accepted,
585
+ asset_id=asset_id,
586
+ )
587
+
588
+
589
+ def classify_image(
590
+ image_path: str,
591
+ labels: Iterable[str] | None = None,
592
+ nsfw_threshold: float = NSFW_THRESHOLD,
593
+ *,
594
+ theme: str | None = None,
595
+ ) -> dict[str, Any]:
596
+ if not image_path:
597
+ raise ValueError("image_path é obrigatório")
598
+
599
+ try:
600
+ with open(image_path, "rb") as file:
601
+ file_bytes = file.read()
602
+ except FileNotFoundError as error:
603
+ raise ValueError(f"Arquivo não encontrado: {image_path}") from error
604
+
605
+ if not file_bytes:
606
+ raise ValueError("Arquivo de imagem vazio.")
607
+
608
+ image_hash = hashlib.sha256(file_bytes).hexdigest()
609
+
610
+ try:
611
+ with Image.open(io.BytesIO(file_bytes)) as image:
612
+ image.load()
613
+ return get_classifier().classify_pil(
614
+ image,
615
+ labels=labels,
616
+ nsfw_threshold=nsfw_threshold,
617
+ image_hash=image_hash,
618
+ theme=theme,
619
+ )
620
+ except (UnidentifiedImageError, OSError) as error:
621
+ raise ValueError("Arquivo inválido: não foi possível decodificar a imagem.") from error
622
+
623
+
624
+ def classify_image_bytes(
625
+ image_bytes: bytes,
626
+ labels: Iterable[str] | None = None,
627
+ nsfw_threshold: float = NSFW_THRESHOLD,
628
+ *,
629
+ asset_id: str | None = None,
630
+ asset_sha256: str | None = None,
631
+ theme: str | None = None,
632
+ similar_threshold: float | None = None,
633
+ similar_limit: int | None = None,
634
+ ) -> dict[str, Any]:
635
+ if not image_bytes:
636
+ raise ValueError("Nenhum conteúdo de imagem recebido.")
637
+
638
+ image_hash = (str(asset_sha256).strip().lower() if asset_sha256 else "") or hashlib.sha256(image_bytes).hexdigest()
639
+
640
+ try:
641
+ with Image.open(io.BytesIO(image_bytes)) as image:
642
+ image.load()
643
+ return get_classifier().classify_pil(
644
+ image,
645
+ labels=labels,
646
+ nsfw_threshold=nsfw_threshold,
647
+ image_hash=image_hash,
648
+ asset_id=asset_id,
649
+ theme=theme,
650
+ similar_threshold=similar_threshold,
651
+ similar_limit=similar_limit,
652
+ )
653
+ except (UnidentifiedImageError, OSError) as error:
654
+ raise ValueError("Upload inválido: não foi possível decodificar a imagem.") from error