@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.
- package/.clusterfuzzlite/Dockerfile +10 -0
- package/.env.example +907 -0
- package/.github/codeql/codeql-config.yml +10 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/codeql.yml +106 -0
- package/.github/workflows/db-migration-check.yml +98 -0
- package/.github/workflows/dependency-review.yml +22 -0
- package/.github/workflows/deploy.yml +95 -0
- package/.github/workflows/release.yml +106 -0
- package/.github/workflows/security-attest-provenance.yml +51 -0
- package/.github/workflows/security-gitleaks.yml +34 -0
- package/.github/workflows/security-runner-hardening.yml +31 -0
- package/.github/workflows/security-scorecard.yml +44 -0
- package/.github/workflows/security-zap-baseline.yml +44 -0
- package/.github/workflows/security-zap-full-scan.yml +43 -0
- package/.github/workflows/security-zizmor.yml +36 -0
- package/.github/workflows/wiki-sync.yml +44 -0
- package/.gitleaks.toml +15 -0
- package/.prettierrc +34 -0
- package/CODE_OF_CONDUCT.md +114 -0
- package/LICENSE +56 -0
- package/README.md +110 -0
- package/SECURITY.md +110 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +92 -0
- package/app/configParts/baileysConfig.js +1818 -0
- package/app/configParts/groupUtils.js +692 -0
- package/app/configParts/loggerConfig.js +394 -0
- package/app/configParts/messagePersistenceService.js +305 -0
- package/app/connection/baileysCompatibility.test.js +40 -0
- package/app/connection/baileysDbAuthState.js +344 -0
- package/app/connection/socketController.js +2243 -0
- package/app/controllers/messageController.js +7 -0
- package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
- package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
- package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
- package/app/controllers/messageProcessingPipeline.js +699 -0
- package/app/modules/adminModule/AGENT.md +4056 -0
- package/app/modules/adminModule/adminAiHelpService.js +56 -0
- package/app/modules/adminModule/adminConfigRuntime.js +177 -0
- package/app/modules/adminModule/commandConfig.json +7122 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
- package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
- package/app/modules/adminModule/groupEventHandlers.js +399 -0
- package/app/modules/aiModule/AGENT.md +547 -0
- package/app/modules/aiModule/aiAiHelpService.js +14 -0
- package/app/modules/aiModule/aiConfigRuntime.js +135 -0
- package/app/modules/aiModule/catCommand.js +967 -0
- package/app/modules/aiModule/commandConfig.json +981 -0
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/gameModule/AGENT.md +196 -0
- package/app/modules/gameModule/commandConfig.json +366 -0
- package/app/modules/gameModule/diceCommand.js +42 -0
- package/app/modules/gameModule/gameAiHelpService.js +14 -0
- package/app/modules/gameModule/gameConfigRuntime.js +68 -0
- package/app/modules/menuModule/AGENT.md +205 -0
- package/app/modules/menuModule/commandConfig.json +366 -0
- package/app/modules/menuModule/common.js +316 -0
- package/app/modules/menuModule/menuAiHelpService.js +14 -0
- package/app/modules/menuModule/menuConfigRuntime.js +68 -0
- package/app/modules/menuModule/menus.js +66 -0
- package/app/modules/playModule/AGENT.md +321 -0
- package/app/modules/playModule/commandConfig.json +584 -0
- package/app/modules/playModule/playAiHelpService.js +14 -0
- package/app/modules/playModule/playCommand.js +1417 -0
- package/app/modules/playModule/playConfigRuntime.js +68 -0
- package/app/modules/quoteModule/AGENT.md +199 -0
- package/app/modules/quoteModule/commandConfig.json +366 -0
- package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
- package/app/modules/quoteModule/quoteCommand.js +842 -0
- package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/AGENT.md +229 -0
- package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
- package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/AGENT.md +320 -0
- package/app/modules/statsModule/commandConfig.json +540 -0
- package/app/modules/statsModule/globalRankingCommand.js +64 -0
- package/app/modules/statsModule/rankingCommand.js +41 -0
- package/app/modules/statsModule/rankingCommon.js +1305 -0
- package/app/modules/statsModule/statsAiHelpService.js +14 -0
- package/app/modules/statsModule/statsConfigRuntime.js +68 -0
- package/app/modules/stickerModule/AGENT.md +692 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/commandConfig.json +1216 -0
- package/app/modules/stickerModule/convertToWebp.js +367 -0
- package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
- package/app/modules/stickerModule/stickerCommand.js +446 -0
- package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
- package/app/modules/stickerModule/stickerTextCommand.js +653 -0
- package/app/modules/stickerPackModule/AGENT.md +215 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
- package/app/modules/stickerPackModule/commandConfig.json +387 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
- package/app/modules/stickerPackModule/domainEvents.js +52 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
- package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
- package/app/modules/stickerPackModule/stickerPackService.js +733 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
- package/app/modules/systemMetricsModule/AGENT.md +193 -0
- package/app/modules/systemMetricsModule/commandConfig.json +344 -0
- package/app/modules/systemMetricsModule/pingCommand.js +399 -0
- package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
- package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
- package/app/modules/tiktokModule/AGENT.md +196 -0
- package/app/modules/tiktokModule/commandConfig.json +366 -0
- package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
- package/app/modules/tiktokModule/tiktokCommand.js +716 -0
- package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
- package/app/modules/userModule/AGENT.md +200 -0
- package/app/modules/userModule/commandConfig.json +386 -0
- package/app/modules/userModule/userAiHelpService.js +14 -0
- package/app/modules/userModule/userCommand.js +1155 -0
- package/app/modules/userModule/userConfigRuntime.js +68 -0
- package/app/modules/waifuPicsModule/AGENT.md +431 -0
- package/app/modules/waifuPicsModule/commandConfig.json +780 -0
- package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
- package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
- package/app/observability/metrics.js +766 -0
- package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
- package/app/services/ai/aiLearningRepository.js +400 -0
- package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
- package/app/services/ai/commandConfigEnrichmentService.js +452 -0
- package/app/services/ai/commandConfigValidationService.js +443 -0
- package/app/services/ai/commandToolBuilderService.js +192 -0
- package/app/services/ai/conversationRouterService.js +516 -0
- package/app/services/ai/geminiService.js +115 -0
- package/app/services/ai/geminiService.test.js +87 -0
- package/app/services/ai/globalModuleAiHelpService.js +1412 -0
- package/app/services/ai/globalToolCallingService.js +203 -0
- package/app/services/ai/messageCommandExecutionService.js +391 -0
- package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
- package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
- package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
- package/app/services/ai/moduleToolExecutorService.js +464 -0
- package/app/services/ai/moduleToolRegistryService.js +178 -0
- package/app/services/ai/toolCandidateSelectorService.js +781 -0
- package/app/services/auth/googleWebLinkService.js +80 -0
- package/app/services/auth/whatsappLoginLinkService.js +230 -0
- package/app/services/external/pokeApiService.js +398 -0
- package/app/services/group/groupMetadataService.js +311 -0
- package/app/services/infra/dbWriteQueue.js +874 -0
- package/app/services/infra/featureFlagService.js +131 -0
- package/app/services/infra/queueUtils.js +55 -0
- package/app/services/messaging/captchaService.js +491 -0
- package/app/services/messaging/messagePersistenceService.js +1 -0
- package/app/services/messaging/newsBroadcastService.js +347 -0
- package/app/services/sticker/stickerFocusService.js +347 -0
- package/app/services/sticker/stickerFocusService.test.js +43 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/conversationSessionStore.js +131 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +54 -0
- package/app/utils/antiLink/antiLinkModule.js +700 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
- package/app/workers/aiLearningWorker.js +605 -0
- package/app/workers/commandConfigEnrichmentWorker.js +242 -0
- package/database/index.js +2075 -0
- package/database/init.js +151 -0
- package/database/migrations/.gitkeep +0 -0
- package/database/migrations/20260307_d0_hardening_down.sql +64 -0
- package/database/migrations/20260307_d0_hardening_up.sql +79 -0
- package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
- package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
- package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
- package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
- package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
- package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
- package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
- package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
- package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
- package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
- package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
- package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
- package/database/schema.sql +1186 -0
- package/docker-compose.yml +104 -0
- package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
- package/docs/audits/stickerCatalogController-symbols.md +58 -0
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
- package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
- package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
- package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
- package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
- package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
- package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
- package/docs/seo/satellite-page-template.md +116 -0
- package/docs/seo/satellite-pages-phase1.json +364 -0
- package/docs/wiki/Home.md +120 -0
- package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
- package/docs/wiki/recent-changes-2026-03-08.md +47 -0
- package/ecosystem.prod.config.cjs +135 -0
- package/eslint.config.js +89 -0
- package/index.js +488 -0
- package/ml/clip_classifier/Dockerfile +18 -0
- package/ml/clip_classifier/README.md +118 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/new-logo.png +0 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +35 -0
- package/observability/promtail-config.yml +84 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +144 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/css/commands-react.input.css +71 -0
- package/public/assets/css/create-pack-react.input.css +31 -0
- package/public/assets/css/home-react.input.css +106 -0
- package/public/assets/css/login-react.input.css +58 -0
- package/public/assets/css/stickers-react.input.css +18 -0
- package/public/assets/css/terms-react.input.css +115 -0
- package/public/assets/css/user-react.input.css +57 -0
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/comandos/commands-catalog.json +4517 -0
- package/public/css/api-docs.css +161 -0
- package/public/css/stickers-admin.css +1288 -0
- package/public/css/styles.css +679 -0
- package/public/css/systemadm/admin.css +474 -0
- package/public/css/systemadm/base.css +73 -0
- package/public/css/systemadm/components.css +662 -0
- package/public/css/systemadm/layout.css +229 -0
- package/public/css/systemadm/tokens.css +56 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +235 -0
- package/public/js/apps/commandsReactApp.js +528 -0
- package/public/js/apps/createPackApp.js +1646 -0
- package/public/js/apps/homeReactApp.js +942 -0
- package/public/js/apps/loginReactApp.js +496 -0
- package/public/js/apps/stickersAdminApp.js +1753 -0
- package/public/js/apps/stickersApp.js +3797 -0
- package/public/js/apps/termsReactApp.js +528 -0
- package/public/js/apps/userApp.js +2540 -0
- package/public/js/apps/userProfile/actions.js +66 -0
- package/public/js/apps/userReactApp.js +547 -0
- package/public/js/catalog.js +950 -0
- package/public/pages/api-docs.html +40 -0
- package/public/pages/aup.html +158 -0
- package/public/pages/comandos.html +41 -0
- package/public/pages/dpa.html +227 -0
- package/public/pages/home.html +45 -0
- package/public/pages/licenca.html +182 -0
- package/public/pages/login.html +40 -0
- package/public/pages/notice-and-takedown.html +234 -0
- package/public/pages/politica-de-privacidade.html +251 -0
- package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
- package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
- package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
- package/public/pages/stickers-admin.html +31 -0
- package/public/pages/stickers-create.html +41 -0
- package/public/pages/stickers.html +45 -0
- package/public/pages/suboperadores.html +237 -0
- package/public/pages/termos-de-uso-texto-integral.html +241 -0
- package/public/pages/termos-de-uso.html +41 -0
- package/public/pages/user-password-reset.html +32 -0
- package/public/pages/user-systemadm.html +508 -0
- package/public/pages/user.html +39 -0
- package/public/robots.txt +9 -0
- package/public/site.webmanifest +24 -0
- package/public/sitemap.xml +98 -0
- package/schemas/command-config.schema.json +582 -0
- package/scripts/baileys-compat-smoke.mjs +12 -0
- package/scripts/cache-bust.mjs +142 -0
- package/scripts/deploy.sh +916 -0
- package/scripts/email-broadcast-terms-update.mjs +170 -0
- package/scripts/enrich-command-discovery-fields.mjs +286 -0
- package/scripts/generate-command-config-schema.mjs +273 -0
- package/scripts/generate-commands-catalog.mjs +308 -0
- package/scripts/generate-module-agents.mjs +631 -0
- package/scripts/generate-seo-satellite-pages.mjs +400 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/github-release-notify.mjs +219 -0
- package/scripts/release.sh +599 -0
- package/scripts/run-codeql-local.sh +116 -0
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/security-smoketest.mjs +581 -0
- package/scripts/sticker-catalog-loadtest.mjs +210 -0
- package/scripts/sticker-worker-task.mjs +119 -0
- package/scripts/sync-readme-snapshot.mjs +133 -0
- package/scripts/validate-command-config-schema.mjs +130 -0
- package/scripts/validate-command-configs.mjs +15 -0
- package/scripts/wiki-sync.sh +191 -0
- package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
- package/server/auth/jwt/webJwtService.js +147 -0
- package/server/auth/stickerCatalogAuthContext.js +165 -0
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
- package/server/auth/userPassword/index.js +14 -0
- package/server/auth/userPassword/userPasswordAuthService.js +422 -0
- package/server/auth/userPassword/userPasswordCrypto.js +199 -0
- package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
- package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
- package/server/auth/validation/authSchemas.js +236 -0
- package/server/auth/webAccount/webAccountHandlers.js +1434 -0
- package/server/controllers/admin/adminBanService.js +138 -0
- package/server/controllers/admin/adminPanelHandlers.js +2083 -0
- package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
- package/server/controllers/admin/systemAdminController.js +201 -0
- package/server/controllers/email/emailAutomationController.js +239 -0
- package/server/controllers/metricsController.js +21 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
- package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
- package/server/controllers/sticker/stickerCatalogController.js +4700 -0
- package/server/controllers/system/contactController.js +115 -0
- package/server/controllers/system/githubController.js +137 -0
- package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
- package/server/controllers/system/storageController.js +154 -0
- package/server/controllers/system/systemController.js +135 -0
- package/server/controllers/system/systemMetricsController.js +156 -0
- package/server/controllers/system/visitController.js +90 -0
- package/server/controllers/userController.js +145 -0
- package/server/email/emailAutomationRuntime.js +225 -0
- package/server/email/emailAutomationService.js +125 -0
- package/server/email/emailOutboxRepository.js +282 -0
- package/server/email/emailTemplateService.js +480 -0
- package/server/email/emailTransportService.js +156 -0
- package/server/http/clientIp.js +95 -0
- package/server/http/httpRequestUtils.js +262 -0
- package/server/http/httpRequestUtils.test.js +80 -0
- package/server/http/httpServer.js +180 -0
- package/server/http/requestContext.js +20 -0
- package/server/http/siteRoutingUtils.js +87 -0
- package/server/index.js +1 -0
- package/server/middleware/cachePolicy.js +26 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/endpointRateLimit.js +181 -0
- package/server/middleware/rateLimit.js +70 -0
- package/server/middleware/requireAdminAuth.js +48 -0
- package/server/middleware/securityHeaders.js +97 -0
- package/server/routes/admin/systemAdminRouter.js +64 -0
- package/server/routes/email/emailAutomationRouter.js +46 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +234 -0
- package/server/routes/metrics/metricsRouter.js +58 -0
- package/server/routes/static/staticPageRouter.js +134 -0
- package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
- package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
- package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
- package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
- package/server/routes/sticker/catalogRouter.js +77 -0
- package/server/routes/sticker/stickerApiRouter.js +84 -0
- package/server/routes/sticker/stickerDataRouter.js +145 -0
- package/server/routes/sticker/stickerSiteRouter.js +43 -0
- package/server/routes/user/userApiPaths.js +66 -0
- package/server/routes/user/userRouter.js +65 -0
- package/server/utils/safePath.js +26 -0
- package/utils/logger/loggerModule.js +35 -0
- package/vite.config.mjs +38 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pymysql
|
|
12
|
+
from pymysql.cursors import DictCursor
|
|
13
|
+
|
|
14
|
+
from env_loader import load_project_env
|
|
15
|
+
|
|
16
|
+
load_project_env()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_env_bool(value: str | None, fallback: bool) -> bool:
|
|
20
|
+
if value is None:
|
|
21
|
+
return fallback
|
|
22
|
+
normalized = str(value).strip().lower()
|
|
23
|
+
if normalized in {"1", "true", "yes", "y", "on"}:
|
|
24
|
+
return True
|
|
25
|
+
if normalized in {"0", "false", "no", "n", "off"}:
|
|
26
|
+
return False
|
|
27
|
+
return fallback
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SimilarImageRow:
|
|
32
|
+
image_hash: str
|
|
33
|
+
asset_id: str | None
|
|
34
|
+
embedding: np.ndarray
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EmbeddingStore:
|
|
38
|
+
"""Persistent store for embeddings, feedback and LLM expansion cache."""
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
self.db_host = os.getenv("DB_HOST", "").strip()
|
|
42
|
+
self.db_user = os.getenv("DB_USER", "").strip()
|
|
43
|
+
self.db_password = os.getenv("DB_PASSWORD", "")
|
|
44
|
+
base_db_name = os.getenv("DB_NAME", "").strip()
|
|
45
|
+
node_env = os.getenv("NODE_ENV", "development").strip().lower() or "development"
|
|
46
|
+
suffix = "prod" if node_env == "production" else "dev"
|
|
47
|
+
if base_db_name.endswith("_prod") or base_db_name.endswith("_dev"):
|
|
48
|
+
self.db_name = base_db_name
|
|
49
|
+
else:
|
|
50
|
+
self.db_name = f"{base_db_name}_{suffix}" if base_db_name else ""
|
|
51
|
+
self.db_port = int(os.getenv("DB_PORT", "3306") or 3306)
|
|
52
|
+
|
|
53
|
+
self.enabled = all([self.db_host, self.db_user, self.db_name])
|
|
54
|
+
self._connection: pymysql.Connection | None = None
|
|
55
|
+
self._schema_ready = False
|
|
56
|
+
self._lock = threading.Lock()
|
|
57
|
+
|
|
58
|
+
def _connect(self) -> pymysql.Connection | None:
|
|
59
|
+
if not self.enabled:
|
|
60
|
+
return None
|
|
61
|
+
if self._connection and self._connection.open:
|
|
62
|
+
return self._connection
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
self._connection = pymysql.connect(
|
|
66
|
+
host=self.db_host,
|
|
67
|
+
user=self.db_user,
|
|
68
|
+
password=self.db_password,
|
|
69
|
+
database=self.db_name,
|
|
70
|
+
port=self.db_port,
|
|
71
|
+
charset="utf8mb4",
|
|
72
|
+
cursorclass=DictCursor,
|
|
73
|
+
autocommit=True,
|
|
74
|
+
connect_timeout=5,
|
|
75
|
+
read_timeout=10,
|
|
76
|
+
write_timeout=10,
|
|
77
|
+
)
|
|
78
|
+
except Exception:
|
|
79
|
+
self._connection = None
|
|
80
|
+
self.enabled = False
|
|
81
|
+
return None
|
|
82
|
+
return self._connection
|
|
83
|
+
|
|
84
|
+
def _ensure_schema(self) -> None:
|
|
85
|
+
if not self.enabled:
|
|
86
|
+
return
|
|
87
|
+
with self._lock:
|
|
88
|
+
if self._schema_ready:
|
|
89
|
+
return
|
|
90
|
+
connection = self._connect()
|
|
91
|
+
if connection is None:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
ddl = [
|
|
95
|
+
"""
|
|
96
|
+
CREATE TABLE IF NOT EXISTS clip_image_embedding_cache (
|
|
97
|
+
image_hash CHAR(64) NOT NULL,
|
|
98
|
+
asset_id VARCHAR(64) NULL,
|
|
99
|
+
model_name VARCHAR(80) NOT NULL,
|
|
100
|
+
embedding_dim SMALLINT UNSIGNED NOT NULL,
|
|
101
|
+
embedding MEDIUMBLOB NOT NULL,
|
|
102
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
103
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
104
|
+
PRIMARY KEY (image_hash),
|
|
105
|
+
KEY idx_clip_image_embedding_asset_id (asset_id),
|
|
106
|
+
KEY idx_clip_image_embedding_model (model_name)
|
|
107
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
108
|
+
""",
|
|
109
|
+
"""
|
|
110
|
+
CREATE TABLE IF NOT EXISTS clip_label_embedding_cache (
|
|
111
|
+
model_name VARCHAR(80) NOT NULL,
|
|
112
|
+
label VARCHAR(191) NOT NULL,
|
|
113
|
+
embedding_dim SMALLINT UNSIGNED NOT NULL,
|
|
114
|
+
embedding MEDIUMBLOB NOT NULL,
|
|
115
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
116
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
117
|
+
PRIMARY KEY (model_name, label)
|
|
118
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
119
|
+
""",
|
|
120
|
+
"""
|
|
121
|
+
CREATE TABLE IF NOT EXISTS clip_llm_label_expansion_cache (
|
|
122
|
+
cache_key CHAR(64) NOT NULL,
|
|
123
|
+
model_name VARCHAR(80) NOT NULL,
|
|
124
|
+
top_labels_json TEXT NOT NULL,
|
|
125
|
+
expansion_json LONGTEXT NOT NULL,
|
|
126
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
127
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
128
|
+
PRIMARY KEY (cache_key)
|
|
129
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
130
|
+
""",
|
|
131
|
+
"""
|
|
132
|
+
CREATE TABLE IF NOT EXISTS clip_image_theme_feedback (
|
|
133
|
+
image_hash CHAR(64) NOT NULL,
|
|
134
|
+
asset_id VARCHAR(64) NULL,
|
|
135
|
+
theme VARCHAR(120) NOT NULL,
|
|
136
|
+
acceptance_count INT UNSIGNED NOT NULL DEFAULT 0,
|
|
137
|
+
total_assignments INT UNSIGNED NOT NULL DEFAULT 0,
|
|
138
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
139
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
140
|
+
PRIMARY KEY (image_hash, theme),
|
|
141
|
+
KEY idx_clip_feedback_asset_id (asset_id),
|
|
142
|
+
KEY idx_clip_feedback_theme (theme)
|
|
143
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
144
|
+
""",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
with connection.cursor() as cursor:
|
|
149
|
+
for statement in ddl:
|
|
150
|
+
cursor.execute(statement)
|
|
151
|
+
self._schema_ready = True
|
|
152
|
+
except Exception:
|
|
153
|
+
self.enabled = False
|
|
154
|
+
self._schema_ready = False
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def stable_labels_key(model_name: str, labels: list[str]) -> str:
|
|
158
|
+
payload = json.dumps({"model": model_name, "labels": labels}, ensure_ascii=False, separators=(",", ":"))
|
|
159
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _serialize_vector(vector: np.ndarray) -> tuple[bytes, int]:
|
|
163
|
+
array = np.asarray(vector, dtype=np.float32).reshape(-1)
|
|
164
|
+
return array.tobytes(), int(array.shape[0])
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _deserialize_vector(raw: bytes, dim: int) -> np.ndarray:
|
|
168
|
+
if not raw:
|
|
169
|
+
return np.empty((0,), dtype=np.float32)
|
|
170
|
+
vector = np.frombuffer(raw, dtype=np.float32)
|
|
171
|
+
if dim > 0 and vector.shape[0] != dim:
|
|
172
|
+
return vector[:dim]
|
|
173
|
+
return vector
|
|
174
|
+
|
|
175
|
+
def get_image_embedding(self, image_hash: str, model_name: str) -> np.ndarray | None:
|
|
176
|
+
if not self.enabled or not image_hash:
|
|
177
|
+
return None
|
|
178
|
+
self._ensure_schema()
|
|
179
|
+
|
|
180
|
+
connection = self._connect()
|
|
181
|
+
if connection is None:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
sql = """
|
|
185
|
+
SELECT embedding, embedding_dim
|
|
186
|
+
FROM clip_image_embedding_cache
|
|
187
|
+
WHERE image_hash = %s AND model_name = %s
|
|
188
|
+
LIMIT 1
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
with connection.cursor() as cursor:
|
|
192
|
+
cursor.execute(sql, (image_hash, model_name))
|
|
193
|
+
row = cursor.fetchone()
|
|
194
|
+
except Exception:
|
|
195
|
+
self.enabled = False
|
|
196
|
+
return None
|
|
197
|
+
if not row:
|
|
198
|
+
return None
|
|
199
|
+
return self._deserialize_vector(row.get("embedding") or b"", int(row.get("embedding_dim") or 0))
|
|
200
|
+
|
|
201
|
+
def save_image_embedding(
|
|
202
|
+
self,
|
|
203
|
+
image_hash: str,
|
|
204
|
+
model_name: str,
|
|
205
|
+
embedding: np.ndarray,
|
|
206
|
+
asset_id: str | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
if not self.enabled or not image_hash:
|
|
209
|
+
return
|
|
210
|
+
self._ensure_schema()
|
|
211
|
+
|
|
212
|
+
payload, dim = self._serialize_vector(embedding)
|
|
213
|
+
sql = """
|
|
214
|
+
INSERT INTO clip_image_embedding_cache (
|
|
215
|
+
image_hash, asset_id, model_name, embedding_dim, embedding
|
|
216
|
+
) VALUES (%s, %s, %s, %s, %s)
|
|
217
|
+
ON DUPLICATE KEY UPDATE
|
|
218
|
+
asset_id = VALUES(asset_id),
|
|
219
|
+
model_name = VALUES(model_name),
|
|
220
|
+
embedding_dim = VALUES(embedding_dim),
|
|
221
|
+
embedding = VALUES(embedding),
|
|
222
|
+
updated_at = CURRENT_TIMESTAMP
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
connection = self._connect()
|
|
226
|
+
if connection is None:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
with connection.cursor() as cursor:
|
|
231
|
+
cursor.execute(sql, (image_hash, asset_id, model_name, dim, payload))
|
|
232
|
+
except Exception:
|
|
233
|
+
self.enabled = False
|
|
234
|
+
|
|
235
|
+
def get_label_embeddings(self, model_name: str, labels: list[str]) -> dict[str, np.ndarray]:
|
|
236
|
+
if not self.enabled or not labels:
|
|
237
|
+
return {}
|
|
238
|
+
self._ensure_schema()
|
|
239
|
+
|
|
240
|
+
connection = self._connect()
|
|
241
|
+
if connection is None:
|
|
242
|
+
return {}
|
|
243
|
+
|
|
244
|
+
placeholders = ", ".join(["%s"] * len(labels))
|
|
245
|
+
sql = f"""
|
|
246
|
+
SELECT label, embedding, embedding_dim
|
|
247
|
+
FROM clip_label_embedding_cache
|
|
248
|
+
WHERE model_name = %s AND label IN ({placeholders})
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
rows: list[dict[str, Any]]
|
|
252
|
+
try:
|
|
253
|
+
with connection.cursor() as cursor:
|
|
254
|
+
cursor.execute(sql, (model_name, *labels))
|
|
255
|
+
rows = cursor.fetchall() or []
|
|
256
|
+
except Exception:
|
|
257
|
+
self.enabled = False
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
output: dict[str, np.ndarray] = {}
|
|
261
|
+
for row in rows:
|
|
262
|
+
label = str(row.get("label") or "")
|
|
263
|
+
if not label:
|
|
264
|
+
continue
|
|
265
|
+
output[label] = self._deserialize_vector(row.get("embedding") or b"", int(row.get("embedding_dim") or 0))
|
|
266
|
+
return output
|
|
267
|
+
|
|
268
|
+
def save_label_embeddings(self, model_name: str, embeddings: dict[str, np.ndarray]) -> None:
|
|
269
|
+
if not self.enabled or not embeddings:
|
|
270
|
+
return
|
|
271
|
+
self._ensure_schema()
|
|
272
|
+
|
|
273
|
+
sql = """
|
|
274
|
+
INSERT INTO clip_label_embedding_cache (
|
|
275
|
+
model_name, label, embedding_dim, embedding
|
|
276
|
+
) VALUES (%s, %s, %s, %s)
|
|
277
|
+
ON DUPLICATE KEY UPDATE
|
|
278
|
+
embedding_dim = VALUES(embedding_dim),
|
|
279
|
+
embedding = VALUES(embedding),
|
|
280
|
+
updated_at = CURRENT_TIMESTAMP
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
connection = self._connect()
|
|
284
|
+
if connection is None:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
payload = []
|
|
288
|
+
for label, vector in embeddings.items():
|
|
289
|
+
serialized, dim = self._serialize_vector(vector)
|
|
290
|
+
payload.append((model_name, label, dim, serialized))
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
with connection.cursor() as cursor:
|
|
294
|
+
cursor.executemany(sql, payload)
|
|
295
|
+
except Exception:
|
|
296
|
+
self.enabled = False
|
|
297
|
+
|
|
298
|
+
def list_image_embeddings(self, model_name: str, limit: int = 3000) -> list[SimilarImageRow]:
|
|
299
|
+
if not self.enabled:
|
|
300
|
+
return []
|
|
301
|
+
self._ensure_schema()
|
|
302
|
+
|
|
303
|
+
connection = self._connect()
|
|
304
|
+
if connection is None:
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
safe_limit = max(50, min(int(limit or 3000), 20000))
|
|
308
|
+
sql = """
|
|
309
|
+
SELECT image_hash, asset_id, embedding, embedding_dim
|
|
310
|
+
FROM clip_image_embedding_cache
|
|
311
|
+
WHERE model_name = %s
|
|
312
|
+
ORDER BY updated_at DESC
|
|
313
|
+
LIMIT %s
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
with connection.cursor() as cursor:
|
|
318
|
+
cursor.execute(sql, (model_name, safe_limit))
|
|
319
|
+
rows = cursor.fetchall() or []
|
|
320
|
+
except Exception:
|
|
321
|
+
self.enabled = False
|
|
322
|
+
return []
|
|
323
|
+
|
|
324
|
+
output: list[SimilarImageRow] = []
|
|
325
|
+
for row in rows:
|
|
326
|
+
image_hash = str(row.get("image_hash") or "")
|
|
327
|
+
if not image_hash:
|
|
328
|
+
continue
|
|
329
|
+
embedding = self._deserialize_vector(row.get("embedding") or b"", int(row.get("embedding_dim") or 0))
|
|
330
|
+
output.append(
|
|
331
|
+
SimilarImageRow(
|
|
332
|
+
image_hash=image_hash,
|
|
333
|
+
asset_id=str(row.get("asset_id")) if row.get("asset_id") is not None else None,
|
|
334
|
+
embedding=embedding,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return output
|
|
338
|
+
|
|
339
|
+
def get_affinity_weight(self, image_hash: str, theme: str) -> float:
|
|
340
|
+
if not self.enabled or not image_hash or not theme:
|
|
341
|
+
return 0.0
|
|
342
|
+
self._ensure_schema()
|
|
343
|
+
|
|
344
|
+
connection = self._connect()
|
|
345
|
+
if connection is None:
|
|
346
|
+
return 0.0
|
|
347
|
+
|
|
348
|
+
sql = """
|
|
349
|
+
SELECT acceptance_count, total_assignments
|
|
350
|
+
FROM clip_image_theme_feedback
|
|
351
|
+
WHERE image_hash = %s AND theme = %s
|
|
352
|
+
LIMIT 1
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
with connection.cursor() as cursor:
|
|
357
|
+
cursor.execute(sql, (image_hash, theme))
|
|
358
|
+
row = cursor.fetchone()
|
|
359
|
+
except Exception:
|
|
360
|
+
self.enabled = False
|
|
361
|
+
return 0.0
|
|
362
|
+
if not row:
|
|
363
|
+
return 0.0
|
|
364
|
+
|
|
365
|
+
acceptance = int(row.get("acceptance_count") or 0)
|
|
366
|
+
total = int(row.get("total_assignments") or 0)
|
|
367
|
+
if total <= 0:
|
|
368
|
+
return 0.0
|
|
369
|
+
return float(acceptance / total)
|
|
370
|
+
|
|
371
|
+
def record_feedback(
|
|
372
|
+
self,
|
|
373
|
+
image_hash: str,
|
|
374
|
+
theme: str,
|
|
375
|
+
accepted: bool,
|
|
376
|
+
asset_id: str | None = None,
|
|
377
|
+
) -> None:
|
|
378
|
+
if not self.enabled or not image_hash or not theme:
|
|
379
|
+
return
|
|
380
|
+
self._ensure_schema()
|
|
381
|
+
|
|
382
|
+
connection = self._connect()
|
|
383
|
+
if connection is None:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
accepted_increment = 1 if accepted else 0
|
|
387
|
+
sql = """
|
|
388
|
+
INSERT INTO clip_image_theme_feedback (
|
|
389
|
+
image_hash, asset_id, theme, acceptance_count, total_assignments
|
|
390
|
+
) VALUES (%s, %s, %s, %s, 1)
|
|
391
|
+
ON DUPLICATE KEY UPDATE
|
|
392
|
+
asset_id = COALESCE(VALUES(asset_id), asset_id),
|
|
393
|
+
acceptance_count = acceptance_count + VALUES(acceptance_count),
|
|
394
|
+
total_assignments = total_assignments + 1,
|
|
395
|
+
updated_at = CURRENT_TIMESTAMP
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
with connection.cursor() as cursor:
|
|
400
|
+
cursor.execute(sql, (image_hash, asset_id, theme, accepted_increment))
|
|
401
|
+
except Exception:
|
|
402
|
+
self.enabled = False
|
|
403
|
+
|
|
404
|
+
def get_llm_expansion(self, cache_key: str) -> dict[str, list[str]] | None:
|
|
405
|
+
if not self.enabled or not cache_key:
|
|
406
|
+
return None
|
|
407
|
+
self._ensure_schema()
|
|
408
|
+
|
|
409
|
+
connection = self._connect()
|
|
410
|
+
if connection is None:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
sql = """
|
|
414
|
+
SELECT expansion_json
|
|
415
|
+
FROM clip_llm_label_expansion_cache
|
|
416
|
+
WHERE cache_key = %s
|
|
417
|
+
LIMIT 1
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
with connection.cursor() as cursor:
|
|
422
|
+
cursor.execute(sql, (cache_key,))
|
|
423
|
+
row = cursor.fetchone()
|
|
424
|
+
except Exception:
|
|
425
|
+
self.enabled = False
|
|
426
|
+
return None
|
|
427
|
+
if not row:
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
raw = row.get("expansion_json")
|
|
431
|
+
if not isinstance(raw, str):
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
parsed = json.loads(raw)
|
|
436
|
+
except json.JSONDecodeError:
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
if not isinstance(parsed, dict):
|
|
440
|
+
return None
|
|
441
|
+
return parsed
|
|
442
|
+
|
|
443
|
+
def save_llm_expansion(
|
|
444
|
+
self,
|
|
445
|
+
cache_key: str,
|
|
446
|
+
model_name: str,
|
|
447
|
+
top_labels: list[str],
|
|
448
|
+
expansion_payload: dict[str, list[str]],
|
|
449
|
+
) -> None:
|
|
450
|
+
if not self.enabled or not cache_key:
|
|
451
|
+
return
|
|
452
|
+
self._ensure_schema()
|
|
453
|
+
|
|
454
|
+
connection = self._connect()
|
|
455
|
+
if connection is None:
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
sql = """
|
|
459
|
+
INSERT INTO clip_llm_label_expansion_cache (
|
|
460
|
+
cache_key, model_name, top_labels_json, expansion_json
|
|
461
|
+
) VALUES (%s, %s, %s, %s)
|
|
462
|
+
ON DUPLICATE KEY UPDATE
|
|
463
|
+
model_name = VALUES(model_name),
|
|
464
|
+
top_labels_json = VALUES(top_labels_json),
|
|
465
|
+
expansion_json = VALUES(expansion_json),
|
|
466
|
+
updated_at = CURRENT_TIMESTAMP
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
with connection.cursor() as cursor:
|
|
471
|
+
cursor.execute(
|
|
472
|
+
sql,
|
|
473
|
+
(
|
|
474
|
+
cache_key,
|
|
475
|
+
model_name,
|
|
476
|
+
json.dumps(top_labels, ensure_ascii=False),
|
|
477
|
+
json.dumps(expansion_payload, ensure_ascii=False),
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
except Exception:
|
|
481
|
+
self.enabled = False
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_project_env() -> None:
|
|
9
|
+
"""Load local and project-root .env files once per process."""
|
|
10
|
+
current_dir = Path(__file__).resolve().parent
|
|
11
|
+
root_dir = current_dir.parent.parent
|
|
12
|
+
|
|
13
|
+
# Keep existing env precedence: do not overwrite already defined values.
|
|
14
|
+
load_dotenv(current_dir / ".env", override=False)
|
|
15
|
+
load_dotenv(root_dir / ".env", override=False)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
|
|
10
|
+
from embedding_store import EmbeddingStore
|
|
11
|
+
from env_loader import load_project_env
|
|
12
|
+
|
|
13
|
+
load_project_env()
|
|
14
|
+
|
|
15
|
+
LLM_EXPANSION_MODEL = os.getenv("LLM_LABEL_EXPANSION_MODEL", "gpt-4.1-mini").strip() or "gpt-4.1-mini"
|
|
16
|
+
LLM_TIMEOUT_MS = max(1000, int(os.getenv("LLM_LABEL_EXPANSION_TIMEOUT_MS", "6000") or 6000))
|
|
17
|
+
|
|
18
|
+
_EMPTY_EXPANSION = {
|
|
19
|
+
"subtags": [],
|
|
20
|
+
"style_traits": [],
|
|
21
|
+
"emotions": [],
|
|
22
|
+
"pack_suggestions": [],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sanitize_list(values: Any, max_items: int = 12) -> list[str]:
|
|
27
|
+
if not isinstance(values, list):
|
|
28
|
+
return []
|
|
29
|
+
cleaned: list[str] = []
|
|
30
|
+
seen: set[str] = set()
|
|
31
|
+
for value in values:
|
|
32
|
+
text = str(value).strip()
|
|
33
|
+
if not text:
|
|
34
|
+
continue
|
|
35
|
+
key = text.lower()
|
|
36
|
+
if key in seen:
|
|
37
|
+
continue
|
|
38
|
+
cleaned.append(text)
|
|
39
|
+
seen.add(key)
|
|
40
|
+
if len(cleaned) >= max_items:
|
|
41
|
+
break
|
|
42
|
+
return cleaned
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _normalize_payload(payload: dict[str, Any] | None) -> dict[str, list[str]]:
|
|
46
|
+
source = payload or {}
|
|
47
|
+
return {
|
|
48
|
+
"subtags": _sanitize_list(source.get("subtags"), 20),
|
|
49
|
+
"style_traits": _sanitize_list(source.get("style_traits"), 12),
|
|
50
|
+
"emotions": _sanitize_list(source.get("emotions"), 10),
|
|
51
|
+
"pack_suggestions": _sanitize_list(source.get("pack_suggestions"), 12),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract_json_dict(text: str) -> dict[str, Any] | None:
|
|
56
|
+
raw = (text or "").strip()
|
|
57
|
+
if not raw:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Accept markdown fenced JSON.
|
|
61
|
+
if raw.startswith("```"):
|
|
62
|
+
raw = raw.strip("`").strip()
|
|
63
|
+
if raw.lower().startswith("json"):
|
|
64
|
+
raw = raw[4:].strip()
|
|
65
|
+
|
|
66
|
+
start = raw.find("{")
|
|
67
|
+
end = raw.rfind("}")
|
|
68
|
+
if start < 0 or end <= start:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
candidate = raw[start : end + 1]
|
|
72
|
+
try:
|
|
73
|
+
parsed = json.loads(candidate)
|
|
74
|
+
except json.JSONDecodeError:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
return parsed if isinstance(parsed, dict) else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_cache_key(model_name: str, top_labels: list[str]) -> str:
|
|
81
|
+
payload = json.dumps({"model": model_name, "labels": top_labels}, ensure_ascii=False, separators=(",", ":"))
|
|
82
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def expand_labels_with_llm(
|
|
86
|
+
*,
|
|
87
|
+
top_labels: list[str],
|
|
88
|
+
store: EmbeddingStore,
|
|
89
|
+
enabled: bool,
|
|
90
|
+
model_name: str = LLM_EXPANSION_MODEL,
|
|
91
|
+
) -> dict[str, list[str]]:
|
|
92
|
+
if not enabled:
|
|
93
|
+
return dict(_EMPTY_EXPANSION)
|
|
94
|
+
|
|
95
|
+
clean_labels = [str(label).strip() for label in top_labels if str(label).strip()][:3]
|
|
96
|
+
if not clean_labels:
|
|
97
|
+
return dict(_EMPTY_EXPANSION)
|
|
98
|
+
|
|
99
|
+
api_key = os.getenv("OPENAI_API_KEY", "").strip()
|
|
100
|
+
if not api_key:
|
|
101
|
+
return dict(_EMPTY_EXPANSION)
|
|
102
|
+
|
|
103
|
+
cache_key = _build_cache_key(model_name=model_name, top_labels=clean_labels)
|
|
104
|
+
cached = store.get_llm_expansion(cache_key)
|
|
105
|
+
if isinstance(cached, dict):
|
|
106
|
+
return _normalize_payload(cached)
|
|
107
|
+
|
|
108
|
+
client = OpenAI(api_key=api_key, timeout=max(1.0, LLM_TIMEOUT_MS / 1000.0))
|
|
109
|
+
|
|
110
|
+
system_prompt = (
|
|
111
|
+
"You are a multimodal taxonomy assistant. "
|
|
112
|
+
"Return only valid JSON with keys: subtags, style_traits, emotions, pack_suggestions."
|
|
113
|
+
)
|
|
114
|
+
user_prompt = (
|
|
115
|
+
"Top labels from an image classifier: "
|
|
116
|
+
f"{json.dumps(clean_labels, ensure_ascii=False)}. "
|
|
117
|
+
"Generate semantic subtags, visual style traits, emotions and related pack themes. "
|
|
118
|
+
"Do not repeat labels verbatim unless needed. Keep each list concise."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
completion = client.chat.completions.create(
|
|
123
|
+
model=model_name,
|
|
124
|
+
response_format={"type": "json_object"},
|
|
125
|
+
messages=[
|
|
126
|
+
{"role": "system", "content": system_prompt},
|
|
127
|
+
{"role": "user", "content": user_prompt},
|
|
128
|
+
],
|
|
129
|
+
temperature=0.2,
|
|
130
|
+
max_tokens=400,
|
|
131
|
+
)
|
|
132
|
+
content = completion.choices[0].message.content if completion.choices else ""
|
|
133
|
+
parsed = _extract_json_dict(content or "")
|
|
134
|
+
normalized = _normalize_payload(parsed)
|
|
135
|
+
except Exception:
|
|
136
|
+
normalized = dict(_EMPTY_EXPANSION)
|
|
137
|
+
|
|
138
|
+
store.save_llm_expansion(
|
|
139
|
+
cache_key=cache_key,
|
|
140
|
+
model_name=model_name,
|
|
141
|
+
top_labels=clean_labels,
|
|
142
|
+
expansion_payload=normalized,
|
|
143
|
+
)
|
|
144
|
+
return normalized
|