@@ -1,17 +1,9 @@ from django.urls import path -from .apis import ( - AchievementAPIView, - AvailableAchievementsAPIView, - IssuedAchievementsAPIView, -) +from .apis import AchievementAPIView, AvailableAchievementsAPIView, IssuedAchievementsAPIView urlpatterns = [ - path( - 'available/', - AvailableAchievementsAPIView.as_view(), - name='available-achievements', - ), + path('available/', AvailableAchievementsAPIView.as_view(), name='available-achievements'), path('/', AchievementAPIView.as_view(), name='achievement'), path('', IssuedAchievementsAPIView.as_view(), name='issued-achievements'), ] @@ -1,12 +1,6 @@ -from authentication.exceptions.business_host_exceptions.already_account import ( - AlreadyAccount, -) -from authentication.exceptions.business_host_exceptions.already_has_plan import ( - AlreadyHasPlan, -) -from authentication.exceptions.business_host_exceptions.already_host import ( - AlreadyHost, -) +from authentication.exceptions.business_host_exceptions.already_account import AlreadyAccount +from authentication.exceptions.business_host_exceptions.already_has_plan import AlreadyHasPlan +from authentication.exceptions.business_host_exceptions.already_host import AlreadyHost __all__ = ( 'AlreadyHasPlan', @@ -1,6 +1,4 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready class AlreadyAccount(BaseAlready): @@ -1,12 +1,8 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready class AlreadyHasPlan(BaseAlready): def msg(self): return dict( - message='user has a paid plan', - user_id=self.user.uid, - plan_id=self.user.payment_plan.plan.uid, + message='user has a paid plan', user_id=self.user.uid, plan_id=self.user.payment_plan.plan.uid ) @@ -1,6 +1,4 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready class AlreadyHost(BaseAlready): @@ -1,6 +1,4 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready __all__ = ('BaseAlready',) @@ -6,8 +6,4 @@ from authentication.models.email_token import EmailToken from authentication.models.user import UTM from authentication.models.user_telegram import TelegramUser from authentication.models.user_vk import VKUser -from authentication.models.whitelist import ( - CompanyIP, - CompanyIPWhitelist, - PolicyWhitelist, -) +from authentication.models.whitelist import CompanyIP, CompanyIPWhitelist, PolicyWhitelist @@ -58,7 +58,9 @@ class BusinessAccount(BaseModel): def clean(self) -> None: if self.group and not self.parent_company == self.group.parent_company: raise ValidationError( - _('Impossible to add this employee to this group which does not belong to this company') + _( + 'Impossible to add this employee to this group which does not belong to this company' + ) ) return super().clean() @@ -41,7 +41,9 @@ class BusinessUserHost(BaseModel): default=BusinessSector.IT, verbose_name=_('Sector'), ) - planned_amount_of_workers = models.IntegerField(default=1, verbose_name=_('Planned amount of workers')) + planned_amount_of_workers = models.IntegerField( + default=1, verbose_name=_('Planned amount of workers') + ) usage_intensity = models.CharField( max_length=50, choices=UsageIntensity.choices, @@ -60,20 +62,30 @@ class BusinessUserHost(BaseModel): default=list, verbose_name=_('Emails token low balance cap'), ) - token_cap_enabled = models.BooleanField(default=False, verbose_name=_('Token low balance cap enabled')) + token_cap_enabled = models.BooleanField( + default=False, verbose_name=_('Token low balance cap enabled') + ) # Judicial information ITN = models.BigIntegerField(null=True, blank=True, verbose_name=_('ITN')) PSRN = models.BigIntegerField(null=True, blank=True, verbose_name=_('PSRN')) - company_name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Name')) + company_name = models.CharField( + max_length=255, null=True, blank=True, verbose_name=_('Name') + ) # Contact information - preferred_name = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Preffered name')) - corporate_email = models.EmailField(null=True, blank=True, verbose_name=_('Corporate email')) + preferred_name = models.CharField( + max_length=50, null=True, blank=True, verbose_name=_('Preffered name') + ) + corporate_email = models.EmailField( + null=True, blank=True, verbose_name=_('Corporate email') + ) corporate_phone = models.CharField( max_length=20, null=True, blank=True, verbose_name=_('Corporate phone') ) - job_title = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Job title')) + job_title = models.CharField( + max_length=64, null=True, blank=True, verbose_name=_('Job title') + ) allowed_models = ArrayField( models.CharField(max_length=64, blank=True), @@ -81,7 +93,9 @@ class BusinessUserHost(BaseModel): verbose_name=_('Allowed models'), ) - is_log_history_enabled = models.BooleanField(default=False, verbose_name=_('Log history enabled')) + is_log_history_enabled = models.BooleanField( + default=False, verbose_name=_('Log history enabled') + ) @property def accounts(self) -> QuerySet[BusinessAccount]: @@ -106,10 +120,7 @@ class BusinessUserHost(BaseModel): @receiver(post_save, sender=BusinessUserHost) def init_ip_whitelist( - sender: type[BusinessUserHost], - instance: BusinessUserHost, - created: bool, - **kwargs, + sender: type[BusinessUserHost], instance: BusinessUserHost, created: bool, **kwargs ): if created: CompanyIPWhitelist.objects.create(company=instance) @@ -2,11 +2,7 @@ import random from typing import TYPE_CHECKING, Optional from uuid import uuid4 -from django.contrib.auth.models import ( - AbstractBaseUser, - BaseUserManager, - PermissionsMixin, -) +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models.signals import post_save @@ -17,19 +13,12 @@ from authentication.models.utm import UTM from core.models import BaseModel if TYPE_CHECKING: - from payments.models.referral_account import ( - ReferralAccount, - ReferralInvite, - ) + from payments.models.referral_account import ReferralAccount, ReferralInvite class CustomUserModelManager(BaseUserManager): def create_user( - self, - username: str, - email: str, - password: Optional[str] = None, - **kwargs, + self, username: str, email: str, password: Optional[str] = None, **kwargs ): """ Creates a custom user with the given fields @@ -127,11 +116,7 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): verbose_name=_('Last name'), ) username = models.CharField( - max_length=100, - unique=True, - null=True, - blank=True, - verbose_name=_('Username'), + max_length=100, unique=True, null=True, blank=True, verbose_name=_('Username') ) email = models.EmailField( max_length=100, @@ -149,7 +134,9 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): is_staff = models.BooleanField(default=False, verbose_name=_('Is staff')) is_superuser = models.BooleanField(default=False, verbose_name=_('Is superuser')) is_confirmed = models.BooleanField(default=True, verbose_name=_('Is email confirmed')) - is_subscribed_to_emails = models.BooleanField(default=True, verbose_name=_('Is subscribed')) + is_subscribed_to_emails = models.BooleanField( + default=True, verbose_name=_('Is subscribed') + ) profile_picture_name = models.CharField( max_length=255, default=None, @@ -175,9 +162,7 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): @property def account_type(self): - from authentication.selectors.account_status_selector import ( - AccountStatusSelector, - ) + from authentication.selectors.account_status_selector import AccountStatusSelector from authentication.selectors.business_account_selector import ( BusinessAccountSelector, ) @@ -200,7 +185,10 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): if not self.profile_picture_name: return None - elif any(prefix in self.profile_picture_name for prefix in ('googleusercontent', 'yandex')): + elif any( + prefix in self.profile_picture_name + for prefix in ('googleusercontent', 'yandex') + ): return self.profile_picture_name return MinIOService().get_object_link('air-profiles', self.profile_picture_name) @@ -251,7 +239,9 @@ def on_user_creation_signal(sender, instance, created, **kwargs): free_plan = PaymentPlanSelector(instance).get_free_plan() PaymentPlanService(instance).subscribe_user_to_plan(free_plan) if not instance.profile_picture_name: - instance.profile_picture_name = instance.LAST_NAME_AVATARS.get(instance.last_name, None) + instance.profile_picture_name = instance.LAST_NAME_AVATARS.get( + instance.last_name, None + ) instance.save() @@ -263,7 +253,9 @@ class UserSetting(models.Model): class TypeChoices(models.TextChoices): SIDEBAR = 'sidebar', 'Сайдбар' - id = models.UUIDField(primary_key=True, editable=False, default=uuid4, verbose_name='ID') + id = models.UUIDField( + primary_key=True, editable=False, default=uuid4, verbose_name='ID' + ) user = models.ForeignKey( CustomUserModel, @@ -6,10 +6,18 @@ from django.utils.translation import gettext_lazy as _ class TelegramUser(models.Model): id = models.BigAutoField(primary_key=True, verbose_name=_('Telegram ID')) is_bot = models.BooleanField(default=False, verbose_name=_('Is bot')) - first_name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_('First name')) - last_name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Last name')) - username = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Username')) - language_code = models.CharField(max_length=4, null=True, blank=True, verbose_name=_('Language')) + first_name = models.CharField( + max_length=255, null=False, blank=False, verbose_name=_('First name') + ) + last_name = models.CharField( + max_length=255, null=True, blank=True, verbose_name=_('Last name') + ) + username = models.CharField( + max_length=255, null=True, blank=True, verbose_name=_('Username') + ) + language_code = models.CharField( + max_length=4, null=True, blank=True, verbose_name=_('Language') + ) is_premium = models.BooleanField(null=True, blank=True, verbose_name=_('Is premium')) is_subscribed_to_air_channel = models.BooleanField( default=False, verbose_name=_('Is subscribed to channel') @@ -22,7 +30,9 @@ class TelegramUser(models.Model): verbose_name=_('User'), ) - phone_number = models.CharField(max_length=20, null=True, blank=True, verbose_name=_('Phonenumber')) + phone_number = models.CharField( + max_length=20, null=True, blank=True, verbose_name=_('Phonenumber') + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at')) @@ -48,7 +48,7 @@ class PolicyWhitelist(BaseModel): emails = ArrayField(models.EmailField(), verbose_name=_('Emails')) def __str__(self): - return f'{",".join(self.emails[:5])}...' + return f"{','.join(self.emails[:5])}..." class Meta: verbose_name = _('Whitelist to cancel policies') @@ -1,8 +1,4 @@ -from authentication.models import ( - BusinessAccount, - BusinessUserHost, - CustomUserModel, -) +from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel from authentication.models.choices import AccountPrivileges @@ -1,8 +1,6 @@ from authentication.models import BusinessAccount, CustomUserModel from authentication.models.business_host import BusinessUserHost -from authentication.services.business_account_service import ( - BusinessAccountService, -) +from authentication.services.business_account_service import BusinessAccountService class BusinessAccountSelector: @@ -5,11 +5,7 @@ from uuid import UUID from django.db.models import BooleanField, Case, Q, Value, When from django.utils.translation import gettext_lazy as _ -from authentication.models import ( - BusinessAccount, - BusinessUserHost, - CustomUserModel, -) +from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AllowedModelsStatus, @@ -48,9 +44,7 @@ class BusinessHostSelector: accounts = accounts.filter(group__isnull=not have_group) if serialize: return BusinessAccountDataSerializer( - accounts, - many=True, - context={'account_type': self.user.account_type}, + accounts, many=True, context={'account_type': self.user.account_type} ) return accounts @@ -61,13 +55,17 @@ class BusinessHostSelector: return host.first() - def get_by_uid(self, uid: UUID, serialize: bool = False) -> BusinessUserHost | BusinessHostSerializer: + def get_by_uid( + self, uid: UUID, serialize: bool = False + ) -> BusinessUserHost | BusinessHostSerializer: host = BusinessUserHost.objects.get(user=self.user, uid=uid) if serialize: return BusinessHostSerializer(host) return host - def get_by_itn(self, itn: int, serialize: bool = False) -> BusinessUserHost | BusinessHostSerializer: + def get_by_itn( + self, itn: int, serialize: bool = False + ) -> BusinessUserHost | BusinessHostSerializer: host = BusinessUserHost.objects.get( user=self.user, ITN=itn, @@ -83,18 +81,20 @@ class BusinessHostSelector: host = self.user.business_account.parent_company else: raise Exception(_("You haven't rights to access host account information")) - return BusinessHostSerializer(host, context={'worker_amount': host.accounts.count()}) + return BusinessHostSerializer( + host, context={'worker_amount': host.accounts.count()} + ) - def get_all_account_statistics( - self, - ) -> BusinessAccountStatisticsSerializer: + def get_all_account_statistics(self) -> BusinessAccountStatisticsSerializer: accounts = self.list_business_accounts() context = dict() for account in accounts: spending_amount = ModelPaymentSelector(account.user).calculate_self_spending() context[account] = spending_amount - return BusinessAccountStatisticsSerializer(accounts, many=True, context={'amounts': context}) + return BusinessAccountStatisticsSerializer( + accounts, many=True, context={'amounts': context} + ) def get_per_model_statistics(self): accounts = self.list_business_accounts() @@ -102,13 +102,19 @@ class BusinessHostSelector: for model_name in self.user.host_account.allowed_models: model_spending = Decimal('0') for account in accounts: - model_spending += ModelPaymentSelector(account.user).calculate_model_spendings(model_name) + model_spending += ModelPaymentSelector( + account.user + ).calculate_model_spendings(model_name) context[model_name] = model_spending - models = NeuronModel.objects.filter(title__in=self.user.host_account.allowed_models) + models = NeuronModel.objects.filter( + title__in=self.user.host_account.allowed_models + ) - return NeuronModelStatisticsSerialiser(models, many=True, context={'amounts': context}) + return NeuronModelStatisticsSerialiser( + models, many=True, context={'amounts': context} + ) def get_allowed_models(self): acc_type = UserSelector(self.user).check_account_type() @@ -119,10 +125,7 @@ class BusinessHostSelector: ) allowed_models = NeuronModel.objects.annotate( is_allowed=Case( - When( - condition=Q(title__in=company.allowed_models), - then=Value(True), - ), + When(condition=Q(title__in=company.allowed_models), then=Value(True)), default=Value(False), output_field=BooleanField(), ) @@ -9,16 +9,9 @@ from authentication.models import CustomUserModel from authentication.models.choices import InvitationStatus from authentication.models.user_telegram import TelegramUser from authentication.models.user_vk import VKUser -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) -from authentication.selectors.business_account_selector import ( - BusinessAccountSelector, -) -from authentication.serializers import ( - SocialAccountSerializer, - UserDetailSerializer, -) +from authentication.selectors.account_status_selector import AccountStatusSelector +from authentication.selectors.business_account_selector import BusinessAccountSelector +from authentication.serializers import SocialAccountSerializer, UserDetailSerializer from payments.models.payment_plan import PaymentPlanUserInfo @@ -27,7 +20,9 @@ class UserSelector: self.user = user @classmethod - def list(cls, serialize: bool = False) -> list[CustomUserModel] | list[UserDetailSerializer]: + def list( + cls, serialize: bool = False + ) -> list[CustomUserModel] | list[UserDetailSerializer]: users = CustomUserModel.objects.prefetch_related( Prefetch( 'payment_plan', @@ -39,7 +34,9 @@ class UserSelector: return users @classmethod - def detail(cls, serialize: bool = True, **kwargs) -> UserDetailSerializer | CustomUserModel: + def detail( + cls, serialize: bool = True, **kwargs + ) -> UserDetailSerializer | CustomUserModel: user_id = kwargs.get('id') user = ( CustomUserModel.objects.filter(uid=user_id) @@ -55,7 +52,9 @@ class UserSelector: user.show_balance = user.business_account.show_balance if not user.show_balance: user.payment_plan.current_token_balance = 0 - user.payment_plan.plan = user.business_account.parent_company.user.payment_plan.plan + user.payment_plan.plan = ( + user.business_account.parent_company.user.payment_plan.plan + ) if serialize: return UserDetailSerializer(user) return user @@ -123,6 +122,8 @@ class UserSelector: """Get newly registered users with further offset if needed""" if date_offset: end_date = register_date + timedelta(days=date_offset + 1) - return CustomUserModel.objects.filter(created_at__date__range=(register_date, end_date)) + return CustomUserModel.objects.filter( + created_at__date__range=(register_date, end_date) + ) return CustomUserModel.objects.filter(created_at__date=register_date) @@ -3,11 +3,7 @@ from typing import Any, OrderedDict, Tuple from django.utils.translation import gettext_lazy as _ -from authentication.models import ( - BusinessAccount, - BusinessUserHost, - CustomUserModel, -) +from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel from authentication.models.choices import InvitationStatus @@ -65,7 +61,9 @@ class BusinessAccountService: elif data['status'] == InvitationStatus.REJECTED: self.reject() else: - raise Exception(_('Invited account can either accept or reject an invitation')) + raise Exception( + _('Invited account can either accept or reject an invitation') + ) return self.account def accept(self): @@ -9,24 +9,12 @@ from authentication.exceptions.business_host_exceptions import ( AlreadyAccount, AlreadyHasPlan, ) -from authentication.exceptions.business_host_exceptions.already_host import ( - AlreadyHost, -) -from authentication.models import ( - BusinessAccount, - BusinessUserHost, - CustomUserModel, -) +from authentication.exceptions.business_host_exceptions.already_host import AlreadyHost +from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel from authentication.models.choices import InvitationStatus -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) -from authentication.selectors.business_account_selector import ( - BusinessAccountSelector, -) -from authentication.selectors.business_host_selector import ( - BusinessHostSelector, -) +from authentication.selectors.account_status_selector import AccountStatusSelector +from authentication.selectors.business_account_selector import BusinessAccountSelector +from authentication.selectors.business_host_selector import BusinessHostSelector from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AccountDataUpdateSerializer, @@ -41,9 +29,7 @@ from authentication.serializers import ( NewBusinessHostSerializer, UserDataSerializer, ) -from authentication.services.business_account_service import ( - BusinessAccountService, -) +from authentication.services.business_account_service import BusinessAccountService from authentication.services.email_service import EmailService from authentication.utils import generate_token from ml_model.models import NeuronModel @@ -65,11 +51,15 @@ class BusinessHostService: ) -> BusinessAccountService: username = f'{self.user.host_account.company_name}_{random_with_N_digits(6)}' password = generate_token(15) - user = CustomUserModel.objects.create_user(username=username, email=email, password=password) + user = CustomUserModel.objects.create_user( + username=username, email=email, password=password + ) user.is_confirmed = True user.save() - PaymentPlanService(user).subscribe_user_to_plan(PaymentPlan.objects.get(price=0, is_corporate=False)) + PaymentPlanService(user).subscribe_user_to_plan( + PaymentPlan.objects.get(price=0, is_corporate=False) + ) account_service = BusinessAccountService.create( user, @@ -79,7 +69,9 @@ class BusinessHostService: ) if token_limit is not None: account_service.update_limit(token_limit) - EmailService(self.user).send_corporate_greeting_email(account_service.account, password) + EmailService(self.user).send_corporate_greeting_email( + account_service.account, password + ) return account_service def create_existing( @@ -122,21 +114,25 @@ class BusinessHostService: serializer.is_valid(raise_exception=True) account = self.create_existing(**serializer.validated_data).account - return BusinessAccountDataSerializer(account, context={'account_type': 'business_account'}) + return BusinessAccountDataSerializer( + account, context={'account_type': 'business_account'} + ) def update_token_limit( self, user: CustomUserModel, amount: Decimal, ): - BusinessAccountSelector.from_user(user, company=self.user.host_account).to_service().update_limit( - amount - ) + BusinessAccountSelector.from_user( + user, company=self.user.host_account + ).to_service().update_limit(amount) - def update_user_invitation_status(self, user: CustomUserModel, new_status: Tuple[str, Any]): - BusinessAccountSelector.from_user(user, company=self.user.host_account).to_service().update_status( - new_status - ) + def update_user_invitation_status( + self, user: CustomUserModel, new_status: Tuple[str, Any] + ): + BusinessAccountSelector.from_user( + user, company=self.user.host_account + ).to_service().update_status(new_status) def update_privileges(self, user: CustomUserModel, new_privileges: str): BusinessAccountSelector.from_user( @@ -168,9 +164,13 @@ class BusinessHostService: if status == InvitationStatus.CANCELLED: return DeletedAccountDataSerializer(user, context={'account_type': 'regular'}) - return BusinessAccountDataSerializer(user.business_account, context={'account_type': account_type}) + return BusinessAccountDataSerializer( + user.business_account, context={'account_type': account_type} + ) - def update_self(self, request: Request, serialize: bool = True) -> BusinessHostSerializer: + def update_self( + self, request: Request, serialize: bool = True + ) -> BusinessHostSerializer: serializer = BusinessHostUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) acc_type = self.user.account_type @@ -179,9 +179,13 @@ class BusinessHostService: if acc_type == 'business_host' else self.user.business_account.parent_company ) - if (token_cap_emails := serializer.validated_data.get('token_cap_emails', None)) is not None: + if ( + token_cap_emails := serializer.validated_data.get('token_cap_emails', None) + ) is not None: company.token_cap_emails = token_cap_emails - if (token_cap_enabled := serializer.validated_data.get('token_cap_enabled', None)) is not None: + if ( + token_cap_enabled := serializer.validated_data.get('token_cap_enabled', None) + ) is not None: company.token_cap_enabled = token_cap_enabled company.save() if serialize: @@ -213,7 +217,9 @@ class BusinessHostService: if AccountStatusSelector(self.user).is_business_account(): raise business_host_exceptions.AlreadyAccount(self.user) - host = BusinessUserHost.objects.create(user=self.user, **serializer.validated_data) + host = BusinessUserHost.objects.create( + user=self.user, **serializer.validated_data + ) host.save() PaymentPlanService(self.user).subscribe_user_to_plan( PaymentPlan.objects.get(price=0, is_corporate=True) @@ -227,7 +233,9 @@ class BusinessHostService: serializer = AddModelsSerializer(data=request.data) serializer.is_valid(raise_exception=True) - models = NeuronModel.objects.filter(title__in=serializer.validated_data['models']).only('title') + models = NeuronModel.objects.filter( + title__in=serializer.validated_data['models'] + ).only('title') for model in models: if model.title not in self.user.host_account.allowed_models: @@ -35,7 +35,9 @@ class EmailService: ) except Exception as exc: logger.exception(exc) - raise Exception('Возникла проблема при регистрации, пожалуйста свяжитесь с администрацией') + raise Exception( + 'Возникла проблема при регистрации, пожалуйста свяжитесь с администрацией' + ) def send_reg_conf_email(self): token = EmailTokenService(self.user).generate_user_token() @@ -110,7 +112,9 @@ class EmailService: ) mail.send(fail_silently=True) - def send_corporate_greeting_email(self, account: BusinessAccount, password: str | None = None): + def send_corporate_greeting_email( + self, account: BusinessAccount, password: str | None = None + ): if self.user.host_account is None: raise Exception(_('Regular users cannot send introductory letters')) message = f""" @@ -6,11 +6,7 @@ import jwt from django.conf import settings from django.contrib.auth.hashers import check_password -from authentication.exceptions import ( - InvalidPassword, - InvalidToken, - InvalidUsername, -) +from authentication.exceptions import InvalidPassword, InvalidToken, InvalidUsername from authentication.models.user import CustomUserModel @@ -82,10 +78,7 @@ class TokenService: @classmethod def _encode( - cls, - *, - payload: dict[str, Any], - token_type: Literal['access', 'refresh'], + cls, *, payload: dict[str, Any], token_type: Literal['access', 'refresh'] ) -> str: issued_at = datetime.now(tz=timezone.utc) jwt_signature = { @@ -7,9 +7,7 @@ from django.db.transaction import atomic from django.utils.translation import gettext_lazy as _ from rest_framework.request import Request -from authentication.exceptions.business_host_exceptions.not_allowed_ip import ( - NotAllowedIP, -) +from authentication.exceptions.business_host_exceptions.not_allowed_ip import NotAllowedIP from authentication.models import ( CompanyIPWhitelist, CustomUserModel, @@ -76,7 +74,9 @@ class UserService: .prefetch_related('user_referral_account') .get() ) - ReferralAccountService.create_invite(referer_account=referer.referral_account, invitee=user) + ReferralAccountService.create_invite( + referer_account=referer.referral_account, invitee=user + ) except CustomUserModel.DoesNotExist: ... return user @@ -84,7 +84,9 @@ class UserService: def create_user_telegram(self, request: Request) -> TelegramUser: serializer = NewUserTelegramSerializer(data=request.data) serializer.is_valid(raise_exception=True) - telegram_user = TelegramUser.objects.create(**serializer.validated_data, user=self.user) + telegram_user = TelegramUser.objects.create( + **serializer.validated_data, user=self.user + ) return telegram_user @classmethod @@ -208,7 +210,10 @@ class UserService: serializer = ChangePasswordSerializer(data=request.data) serializer.is_valid(raise_exception=True) - if serializer.validated_data['password_1'] != serializer.validated_data['password_2']: + if ( + serializer.validated_data['password_1'] + != serializer.validated_data['password_2'] + ): raise Exception(_('Passwords do not match')) user = token.user @@ -230,9 +235,7 @@ class UserService: new_profile_picture = serializer.validated_data.get('profile_picture') if new_profile_picture is not None: new_picture_name = MinIOService().put_object( - new_profile_picture, - f'{self.user.username}.png', - 'air-profiles', + new_profile_picture, f'{self.user.username}.png', 'air-profiles' ) self.user.profile_picture_name = new_picture_name @@ -249,7 +252,10 @@ class UserService: if user is None: raise Exception(_('Current password is wrong')) - if serializer.validated_data['password_1'] != serializer.validated_data['password_2']: + if ( + serializer.validated_data['password_1'] + != serializer.validated_data['password_2'] + ): raise Exception("Passwords don't match") self.user.set_password(serializer.validated_data['password_1']) @@ -260,7 +266,9 @@ class UserService: serializer.is_valid(raise_exception=True) img = serializer.validated_data['new_picture'] - img_name = MinIOService().put_object(img, f'{self.user.username}.png', 'air-profiles') + img_name = MinIOService().put_object( + img, f'{self.user.username}.png', 'air-profiles' + ) self.user.profile_picture_name = img_name self.user.save() @@ -271,15 +279,21 @@ class UserService: @classmethod def exists_in_whitelist(cls, request: Request) -> bool: - return PolicyWhitelist.objects.filter(emails__contains=[request.query_params['email']]).exists() + return PolicyWhitelist.objects.filter( + emails__contains=[request.query_params['email']] + ).exists() @classmethod def list_settings(cls, filters: Q = Q()) -> QuerySet[UserSetting]: return UserSetting.objects.filter(filters) @classmethod - def add_setting(cls, user_id: UUID, device: str, type: str, value: Any) -> UserSetting: - return UserSetting.objects.create(user_id=user_id, device=device, type=type, value=value) + def add_setting( + cls, user_id: UUID, device: str, type: str, value: Any + ) -> UserSetting: + return UserSetting.objects.create( + user_id=user_id, device=device, type=type, value=value + ) @classmethod def update_setting(cls, setting_id: UUID, value: Any) -> None: @@ -298,6 +312,7 @@ def update_profile_picture_social(*args, **kwargs): # Yandex Picture elif response.get('default_avatar_id', None): air_user.profile_picture_name = ( - f'https://avatars.yandex.net/get-yapic/{response["default_avatar_id"]}/islands-retina-50' + f"https://avatars.yandex.net/get-yapic/" + f"{response['default_avatar_id']}/islands-retina-50" ) air_user.save() @@ -165,12 +165,7 @@ class CustomUserModelAdmin(UserAdmin, ExportActionModelAdmin): sheet.append(['Email', 'Дата регистрации', 'Текущий баланс', 'Тип аккаунта']) for user in qs: sheet.append( - [ - user.email, - f'{user.created_at}', - user.balance, - user.account_type, - ] + [user.email, f'{user.created_at}', user.balance, user.account_type] ) response = HttpResponse(content_type='application/ms-excel') response['Content-Disposition'] = 'attachment; filename=users_month_info.xlsx' @@ -183,7 +178,9 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): actions = ['download_statistics_month', 'download_statistics_week'] @admin.action(description='Скачать месячный отчет по выбранным Вайт-листам') - def download_statistics_month(self, request, qs: QuerySet[PolicyWhitelist], *args, **kwargs): + def download_statistics_month( + self, request, qs: QuerySet[PolicyWhitelist], *args, **kwargs + ): end = timezone.now() start = end - timedelta(days=30) wb = Workbook() @@ -202,11 +199,15 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): obj['uid'] for obj in list( chain( - Image.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), - Chat.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), - Copywrite.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values( - 'uid' - ), + Image.objects.filter( + user__email__in=qs.values('emails')[0]['emails'] + ).values('uid'), + Chat.objects.filter( + user__email__in=qs.values('emails')[0]['emails'] + ).values('uid'), + Copywrite.objects.filter( + user__email__in=qs.values('emails')[0]['emails'] + ).values('uid'), ) ) ], @@ -229,12 +230,16 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): ] ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=whitelists_month_info.xlsx' + response['Content-Disposition'] = ( + 'attachment; filename=whitelists_month_info.xlsx' + ) wb.save(response) return response @admin.action(description='Скачать недельный отчет по выбранным Вайт-листам') - def download_statistics_week(self, request, qs: list[PolicyWhitelist], *args, **kwargs): + def download_statistics_week( + self, request, qs: list[PolicyWhitelist], *args, **kwargs + ): end = timezone.now() start = end - timedelta(days=7) wb = Workbook() @@ -253,11 +258,15 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): obj['uid'] for obj in list( chain( - Image.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), - Chat.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), - Copywrite.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values( - 'uid' - ), + Image.objects.filter( + user__email__in=qs.values('emails')[0]['emails'] + ).values('uid'), + Chat.objects.filter( + user__email__in=qs.values('emails')[0]['emails'] + ).values('uid'), + Copywrite.objects.filter( + user__email__in=qs.values('emails')[0]['emails'] + ).values('uid'), ) ) ], @@ -280,7 +289,9 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): ] ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=PolicyWhitelists_month_info.xlsx' + response['Content-Disposition'] = ( + 'attachment; filename=PolicyWhitelists_month_info.xlsx' + ) wb.save(response) return response @@ -340,8 +351,8 @@ class UTMAdmin(admin.ModelAdmin): for utm in qs: sheet.append( [ - f'{utm.utm_source if utm.utm_source else "Источник отсутствует"}: ' - f'{utm.utm_campaign if utm.utm_campaign else "Компания отсутствует"}: ', + f"{utm.utm_source if utm.utm_source else 'Источник отсутствует'}: " + f"{utm.utm_campaign if utm.utm_campaign else 'Компания отсутствует'}: ", PaymentSelector.count_by_utm(utm=utm, from_date=start, to_date=end), PaymentSelector.sum_by_utm(utm=utm, from_date=start, to_date=end), utm.users.filter(created_at__date__range=[start, end]).count(), @@ -400,16 +411,13 @@ class CompanyIPInline(admin.TabularInline): class CompanyIPWhitelistAdmin(admin.ModelAdmin): inlines = [CompanyIPInline] - def log_change(self, request: HttpRequest, object: Any, message: list[dict[str, any]]) -> LogEntry: + def log_change( + self, request: HttpRequest, object: Any, message: list[dict[str, any]] + ) -> LogEntry: ct = ContentType.objects.get_for_model(object, for_concrete_model=False) return [ LogEntry.objects.log_action( - request.user.pk, - ct.pk, - object.pk, - str(object), - 1, - [message_entry], + request.user.pk, ct.pk, object.pk, str(object), 1, [message_entry] ) for message_entry in message if not message_entry.get('changed', None) @@ -3,9 +3,7 @@ from rest_framework.request import Request from rest_framework.views import APIView from authentication.models.choices import AccountPrivileges -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) +from authentication.selectors.account_status_selector import AccountStatusSelector from backend import settings @@ -37,10 +37,4 @@ class ReferralUserResource(ModelResource): class Meta: model = CustomUserModel - fields = ( - 'email', - 'balance', - 'created_at', - 'referer', - 'payments_count', - ) + fields = ('email', 'balance', 'created_at', 'referer', 'payments_count') @@ -16,7 +16,10 @@ class SyncAuthBearer(HttpBearer): try: user_payload = async_to_sync(TokenService.decode)(token=token) return CustomUserModel.objects.get( - **{key: user_payload[f'{key}'] for key in settings.JWT_SETTINGS['encode_attributes']} + **{ + key: user_payload[f'{key}'] + for key in settings.JWT_SETTINGS['encode_attributes'] + } ) except InvalidToken: access = AccessToken.objects.prefetch_related('user').get(token=token) @@ -188,7 +188,9 @@ class BusinessAccountDataSerializer(serializers.Serializer): class BusinessHostUpdateSerializer(serializers.Serializer): - token_cap_emails = serializers.ListField(child=serializers.EmailField(), required=False) + token_cap_emails = serializers.ListField( + child=serializers.EmailField(), required=False + ) token_cap_enabled = serializers.BooleanField(required=False) @@ -253,7 +255,9 @@ class BusinessHostSerializer(serializers.Serializer): corporate_phone = serializers.CharField(required=False) job_title = serializers.CharField(required=False) worker_amount = serializers.SerializerMethodField() - is_ip_whitelist_enabled = serializers.BooleanField(read_only=True, source='ip_whitelist.is_enabled') + is_ip_whitelist_enabled = serializers.BooleanField( + read_only=True, source='ip_whitelist.is_enabled' + ) is_log_history_enabled = serializers.BooleanField(read_only=True) token_cap_emails = serializers.ListField(child=serializers.EmailField()) token_cap_enabled = serializers.BooleanField() @@ -331,4 +335,8 @@ class LogSerializer(serializers.ModelSerializer): @extend_schema_field(OpenApiTypes.STR) def _user(self, obj: LogEntry): - return 'Сотрудник AIR' if obj.content_type.model == 'companyipwhitelist' else obj.user.email + return ( + 'Сотрудник AIR' + if obj.content_type.model == 'companyipwhitelist' + else obj.user.email + ) @@ -6,74 +6,40 @@ from authentication import views urlpatterns = [ path('register', views.UserAPIView.as_view(), name='new-user'), path( - 'mail/resend', - view=views.EmailRegisterResendView.as_view(), - name='email/resend', - ), - path( - 'mail/whitelist', - view=views.MailWhitelist.as_view(), - name='mail-whitelist', + 'mail/resend', view=views.EmailRegisterResendView.as_view(), name='email/resend' ), + path('mail/whitelist', view=views.MailWhitelist.as_view(), name='mail-whitelist'), path('me', views.UserAPIView.as_view(), name='me'), path( - 'register-telegram', - views.UserTelegramAPIView.as_view(), - name='new-user-telegram', + 'register-telegram', views.UserTelegramAPIView.as_view(), name='new-user-telegram' ), path('register/vk', views.UserVKAPIView.as_view(), name='new-user-vk'), path('login', views.UserLoginAPIView.as_view(), name='ml_model-login'), path('login-social/', include('drf_social_oauth2.urls', namespace='drf')), path( - 'login-from-token', - views.LoginFromTokenAPIView.as_view(), - name='login-from-token', - ), - path( - 'login-telegram', - views.UserLoginTelegramView.as_view(), - name='login-telegram', + 'login-from-token', views.LoginFromTokenAPIView.as_view(), name='login-from-token' ), + path('login-telegram', views.UserLoginTelegramView.as_view(), name='login-telegram'), path('login/vk', views.UserLoginVKView.as_view(), name='login-vk'), path('logout', views.UserLogoutAPIView.as_view(), name='logout'), path('remove', views.DeleteUserAPIView.as_view(), name='remove'), path('confirm', views.ConfirmUserAPIView.as_view(), name='confirm-user'), - path( - 'change-pass', - views.ChangePasswordAPIView.as_view(), - name='change-password', - ), + path('change-pass', views.ChangePasswordAPIView.as_view(), name='change-password'), path( 'update-pass', views.RequestPasswordChangeAPIView.as_view(), name='request-password-update', ), - path( - 'user-data', - views.UpdateUserDataAPIView.as_view(), - name='update-user-data', - ), - path( - 'email-sub', - views.UpdateEmailSubscriptionAPIView.as_view(), - name='email-sub', - ), - path( - 'reset-pass', - views.UpdatePasswordAPIView.as_view(), - name='update-password', - ), + path('user-data', views.UpdateUserDataAPIView.as_view(), name='update-user-data'), + path('email-sub', views.UpdateEmailSubscriptionAPIView.as_view(), name='email-sub'), + path('reset-pass', views.UpdatePasswordAPIView.as_view(), name='update-password'), path( 'reset-profile-pic', views.UpdateProfilePictureAPIView.as_view(), name='update-profile-pic', ), path('token/refresh', TokenRefreshView.as_view(), name='refresh-jwt'), - path( - 'business-host', - views.BusinessHostAPIView.as_view(), - name='business-host', - ), + path('business-host', views.BusinessHostAPIView.as_view(), name='business-host'), path( 'business-host/create', views.StartHostRegistrationAPIView.as_view(), @@ -86,9 +52,7 @@ urlpatterns = [ ), path('business-host/logs/', views.LogsAPIView.as_view()), path( - 'business-host/accounts', - views.HostWorkersAPIView.as_view(), - name='hosts-workers', + 'business-host/accounts', views.HostWorkersAPIView.as_view(), name='hosts-workers' ), path('business-host/download-expenses', views.ExtractHostExpenses.as_view()), path( @@ -119,10 +83,7 @@ urlpatterns = [ name='download-business-report', ), path('business-groups/', views.BusinessGroupsAPIView.as_view()), - path( - 'business-groups//', - views.BusinessGroupAPIView.as_view(), - ), + path('business-groups//', views.BusinessGroupAPIView.as_view()), path( 'business-groups//accounts//', views.BusinessGroupAccountsAPIView.as_view(), @@ -19,9 +19,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from authentication.exceptions import BaseAlready -from authentication.exceptions.business_host_exceptions.not_allowed_ip import ( - NotAllowedIP, -) +from authentication.exceptions.business_host_exceptions.not_allowed_ip import NotAllowedIP from authentication.models.business_group import BusinessGroup from authentication.models.business_host import BusinessUserHost from authentication.models.choices import AccountPrivileges, InvitationStatus @@ -33,9 +31,7 @@ from authentication.permissions import ( IsTelegramAirBot, IsVKMiniApp, ) -from authentication.selectors.business_host_selector import ( - BusinessHostSelector, -) +from authentication.selectors.business_host_selector import BusinessHostSelector from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AccountDataUpdateSerializer, @@ -68,9 +64,7 @@ from authentication.serializers import ( UpdateUserDataSerializer, UserDataSerializer, ) -from authentication.services.business_account_service import ( - BusinessAccountService, -) +from authentication.services.business_account_service import BusinessAccountService from authentication.services.business_host_service import BusinessHostService from authentication.services.email_token_service import EmailTokenService from authentication.services.user_services import EmailService, UserService @@ -97,7 +91,9 @@ class UserLoginAPIView(APIView): logger.exception(err) return Response( {'detail': f'{err}'}, - status=status.HTTP_400_BAD_REQUEST if err != NotAllowedIP else status.HTTP_403_FORBIDDEN, + status=status.HTTP_400_BAD_REQUEST + if err != NotAllowedIP + else status.HTTP_403_FORBIDDEN, ) @@ -132,8 +128,7 @@ class UserLoginTelegramView(APIView): permission_classes = (IsTelegramAirBot,) @extend_schema( - request=LoginTelegramUserSerializer, - responses={200: UserDataSerializer}, + request=LoginTelegramUserSerializer, responses={200: UserDataSerializer} ) def post(self, request, *args, **kwargs): """Login user from Telegram Bot by Telegram ID. Bot rights required.""" @@ -165,8 +160,7 @@ class UserLogoutAPIView(APIView): try: UserService(self.request.user).logout_user(request) return Response( - {'detail': 'User logout successfully'}, - status=status.HTTP_200_OK, + {'detail': 'User logout successfully'}, status=status.HTTP_200_OK ) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -243,7 +237,9 @@ class UserStatusAPIView(APIView): """Get user status: regular, business host, business admin or business account""" try: user_status = UserSelector(self.request.user).check_account_type() - response = AccountStatusSerializer(self.request.user, context={'status': user_status}) + response = AccountStatusSerializer( + self.request.user, context={'status': user_status} + ) return Response(response.data, status=status.HTTP_200_OK) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -276,12 +272,13 @@ class BusinessHostAPIView(APIView): return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @extend_schema( - request=BusinessHostUpdateSerializer, - responses={200: BusinessHostSerializer}, + request=BusinessHostUpdateSerializer, responses={200: BusinessHostSerializer} ) def put(self, request, *args, **kwargs): try: - result = BusinessHostService(self.request.user).update_self(request, serialize=True) + result = BusinessHostService(self.request.user).update_self( + request, serialize=True + ) return Response(result.data, status=status.HTTP_200_OK) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -302,9 +299,7 @@ class HostWorkersAPIView(APIView): @extend_schema( parameters=[ OpenApiParameter( - 'type', - enum=[value for value, _ in AccountPrivileges.choices], - many=True, + 'type', enum=[value for value, _ in AccountPrivileges.choices], many=True ), OpenApiParameter('have_group', bool), ], @@ -384,8 +379,7 @@ class ChangePasswordAPIView(APIView): permission_classes = (IsAnonymous,) @extend_schema( - parameters=[OpenApiParameter('token', str)], - request=UpdatePasswordSerializer, + parameters=[OpenApiParameter('token', str)], request=UpdatePasswordSerializer ) def post(self, request, *args, **kwargs): """Edit user password from email.""" @@ -444,8 +438,7 @@ class UpdateProfilePictureAPIView(APIView): try: UserService(self.request.user).update_profile_picture(request) return Response( - {'detail': 'profile picture updated'}, - status=status.HTTP_200_OK, + {'detail': 'profile picture updated'}, status=status.HTTP_200_OK ) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -523,7 +516,9 @@ class AllowedHostModelsAPIView(APIView): def delete(self, request, *args, **kwargs): """Delete all allowed models for company user.""" try: - result = BusinessHostService(self.request.user).remove_available_models(request) + result = BusinessHostService(self.request.user).remove_available_models( + request + ) return Response(result.data, status=status.HTTP_200_OK) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -550,7 +545,9 @@ class AccountInvitationAPIView(APIView): """Change invitation to company from email - Accept.""" try: token = EmailTokenService.get_token(request.query_params.get('token')) - BusinessAccountService.from_user(token.user).update(data=dict(status=InvitationStatus.ACCEPTED)) + BusinessAccountService.from_user(token.user).update( + data=dict(status=InvitationStatus.ACCEPTED) + ) return redirect(settings.MAIN_SITE_URL) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -572,8 +569,7 @@ class ExtractBusinessAccountQueriesAPIView(APIView): start = datetime.fromisoformat( request.query_params.get( - 'start_date', - (timezone.now() - timedelta(days=30)).strftime('%Y-%m-%d'), + 'start_date', (timezone.now() - timedelta(days=30)).strftime('%Y-%m-%d') ) ) end = datetime.fromisoformat( @@ -625,7 +621,9 @@ class ExtractBusinessAccountQueriesAPIView(APIView): ] ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=business_accounts_info.xlsx' + response['Content-Disposition'] = ( + 'attachment; filename=business_accounts_info.xlsx' + ) wb.save(response) return response @@ -658,8 +656,7 @@ class BusinessGroupsAPIView(APIView): return Response(BusinessGroupsSerializer(groups, many=True).data, status=200) @extend_schema( - request=BusinessGroupSerializer, - responses={201: BusinessGroupSerializer}, + request=BusinessGroupSerializer, responses={201: BusinessGroupSerializer} ) def post(self, request, *args, **kwargs): serializer = BusinessGroupSerializer(data=request.data) @@ -696,13 +693,17 @@ class BusinessGroupAPIView(APIView): class BusinessGroupAccountsAPIView(APIView): def post(self, request, group_id: UUID, acc_email: UUID, *args, **kwargs): group = BusinessGroup.objects.get(uid=group_id) - business_account = BusinessAccountService.from_user(UserSelector.get_by_email(acc_email)).account + business_account = BusinessAccountService.from_user( + UserSelector.get_by_email(acc_email) + ).account business_account.group = group business_account.save() return Response(status=status.HTTP_200_OK) def delete(self, request, group_id: UUID, acc_email: UUID, *args, **kwargs): - business_account = BusinessAccountService.from_user(UserSelector.get_by_email(acc_email)).account + business_account = BusinessAccountService.from_user( + UserSelector.get_by_email(acc_email) + ).account business_account.group = None business_account.save() return Response(status=status.HTTP_204_NO_CONTENT) @@ -722,7 +723,9 @@ class LogsAPIView(APIView): def get(self, request, *args, **kwargs): logs = LogEntry.objects.filter( action_time__range=[ - request.query_params.get('from-date', timezone.now() - timedelta(days=365)), + request.query_params.get( + 'from-date', timezone.now() - timedelta(days=365) + ), request.query_params.get('to-date', timezone.now()), ] ) @@ -736,7 +739,9 @@ class LogsAPIView(APIView): company.user, *[acc.user for acc in company.accounts.filter(account_privileges='admin')], ] - keys = [order['uid'] for order in APIKey.objects.filter(user__in=users).values('uid')] + keys = [ + order['uid'] for order in APIKey.objects.filter(user__in=users).values('uid') + ] whitelist = company.ip_whitelist match request.query_params.get('log-identity'): case 'public-api': @@ -754,10 +759,7 @@ class LogsAPIView(APIView): case _: logs = logs.filter( object_id__in=[whitelist.pk, *keys], - content_type__app_label__in=[ - 'public_api', - 'authentication', - ], + content_type__app_label__in=['public_api', 'authentication'], content_type__model__in=['apikey', 'companyipwhitelist'], ) return Response(LogSerializer(logs, many=True).data) @@ -781,7 +783,9 @@ class ExtractHostExpenses(APIView): *[acc['user'] for acc in company.accounts.values('user')], ], created_at__range=[ - request.query_params.get('from_date', timezone.now() - timedelta(days=30)), + request.query_params.get( + 'from_date', timezone.now() - timedelta(days=30) + ), request.query_params.get('to_date', timezone.now()), ], ) @@ -792,7 +796,9 @@ class ExtractHostExpenses(APIView): *[acc['user'] for acc in company.accounts.values('user')], ], created_at__range=[ - request.query_params.get('from_date', timezone.now() - timedelta(days=30)), + request.query_params.get( + 'from_date', timezone.now() - timedelta(days=30) + ), request.query_params.get('to_date', timezone.now()), ], ) @@ -813,6 +819,8 @@ class ExtractHostExpenses(APIView): ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=business_accounts_info.xlsx' + response['Content-Disposition'] = ( + 'attachment; filename=business_accounts_info.xlsx' + ) wb.save(response) return response @@ -140,7 +140,8 @@ SOCIALACCOUNT_EMAIL_VERIFICATION = 'none' SOCIALACCOUNT_EMAIL_REQUIRED = False AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + 'NAME': 'django.contrib.auth.' + 'password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', @@ -240,7 +241,9 @@ MINIO_PORT = env.str('MINIO_PORT', default='9000') MINIO_ENDPOINT = env.str('MINIO_ENDPOINT') MINIO_USE_HTTPS = env.bool('MINIO_USE_HTTPS', default=False) MINIO_EXTERNAL_ENDPOINT = env.str('MINIO_EXTERNAL_ENDPOINT', default='localhost:9000') -MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = env.bool('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=False) +MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = env.bool( + 'MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=False +) MINIO_ACCESS_KEY = env.str('MINIO_ACCESS_KEY') MINIO_SECRET_KEY = env.str('MINIO_SECRET_KEY') @@ -331,7 +334,9 @@ UPSCALE_MULTIPLIER_HOST = env.str('UPSCALE_MULTIPLIER_HOST', 'packet:8080') # Payments YOOKASSA_ACCOUNT_ID = env.str('YOOKASSA_ACCOUNT_ID', default='defaultapikey') YOOKASSA_SECRET_KEY = env.str('YOOKASSA_SECRET_KEY', default='defaultapikey') -YOOKASSA_RESULT_PAYMENT_URL = env.str('YOOKASSA_RESULT_PAYMENT_URL', default='defaultapikey') +YOOKASSA_RESULT_PAYMENT_URL = env.str( + 'YOOKASSA_RESULT_PAYMENT_URL', default='defaultapikey' +) RECURRENT_RATE = env.str('RECURRENT_RATE', 'days') # Email EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' @@ -344,8 +349,12 @@ EMAIL_HOST_PASSWORD = env.str('EMAIL_HOST_PASSWORD', default='defaultpass') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER USER_CONFIRMATION_URL = env.str('USER_CONFIRMATION_URL', default='http://localhost:3000') -USER_PASSWORD_RESET_URL = env.str('USER_PASSWORD_RESET_URL', default='http://localhost:3000') -INVITATION_RESPONSE_URL = env.str('INVITATION_RESPONSE_URL', default='http://localhost:3000') +USER_PASSWORD_RESET_URL = env.str( + 'USER_PASSWORD_RESET_URL', default='http://localhost:3000' +) +INVITATION_RESPONSE_URL = env.str( + 'INVITATION_RESPONSE_URL', default='http://localhost:3000' +) MAIN_SITE_URL = env.str('MAIN_SITE_URL', default='http://localhost:3000') @@ -372,12 +381,7 @@ ADMIN_SETTINGS = { 'media', 'copywrite', ], - 'excludes': [ - 'django_celery_beat', - 'social_django', - 'authtoken', - 'auth', - ], + 'excludes': ['django_celery_beat', 'social_django', 'authtoken', 'auth'], }, } @@ -9,11 +9,7 @@ from django.utils.translation import gettext_lazy as _ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from ninja import NinjaAPI -from authentication.exceptions import ( - InvalidPassword, - InvalidToken, - InvalidUsername, -) +from authentication.exceptions import InvalidPassword, InvalidToken, InvalidUsername from backend.public import urlpatterns as public_urlpatterns api = NinjaAPI(title='AIR API', version='1.0.0') @@ -28,7 +24,9 @@ logger = logging.getLogger(__name__) @api.exception_handler(ObjectDoesNotExist) def object_does_not_exists_error_handler(request, exc: ObjectDoesNotExist): logger.exception(exc) - return api.create_response(request, {'message': _('Requested object does not exists')}, status=404) + return api.create_response( + request, {'message': _('Requested object does not exists')}, status=404 + ) @api.exception_handler(InvalidToken) @@ -1,10 +0,0 @@ -from django.db import models -from django.forms import TextInput - - -class ColorField(models.CharField): - max_length = 6 - - def formfield(self, **kwargs): - kwargs['widget'] = TextInput(attrs={'type': 'color'}) - return super().formfield(**kwargs) @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-30 03:00+0300\n" +"POT-Creation-Date: 2025-03-24 17:41+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,8 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:47 -#: stories/models.py:18 +#: achievements/admin.py:11 achievements/models.py:19 stories/models.py:18 msgid "Icon" msgstr "" @@ -31,21 +30,21 @@ msgstr "" msgid "Achievements" msgstr "" -#: achievements/models.py:14 ml_model/models.py:19 ml_model/models.py:39 -#: ml_model/models.py:72 ml_model/models.py:180 +#: achievements/models.py:14 ml_model/models.py:16 ml_model/models.py:38 +#: ml_model/models.py:145 msgid "Slug" msgstr "" -#: achievements/models.py:16 ml_model/models.py:70 ml_model/models.py:179 -#: ml_model/models.py:268 payments/models/payment.py:52 +#: achievements/models.py:16 ml_model/models.py:36 ml_model/models.py:143 +#: ml_model/models.py:232 payments/models/payment.py:53 msgid "Description" msgstr "" #: achievements/models.py:43 authentication/models/business_host.py:21 -#: authentication/models/email_token.py:12 authentication/models/user.py:241 -#: authentication/models/user.py:242 authentication/models/user_telegram.py:22 +#: authentication/models/email_token.py:12 authentication/models/user.py:229 +#: authentication/models/user.py:230 authentication/models/user_telegram.py:30 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:61 +#: payments/models/payment.py:26 payments/models/payment_plan.py:63 msgid "User" msgstr "" @@ -62,8 +61,8 @@ msgstr "" msgid "Issued achievement" msgstr "" -#: authentication/models/business_account.py:16 payments/models/promocode.py:72 -#: tools/public_api/models.py:33 +#: authentication/models/business_account.py:16 payments/models/promocode.py:69 +#: tools/public_api/models.py:35 msgid "Owner" msgstr "" @@ -86,7 +85,7 @@ msgid "Acceptance" msgstr "" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:43 +#: authentication/models/business_group.py:21 tools/public_api/models.py:45 msgid "Token limit" msgstr "" @@ -94,23 +93,23 @@ msgstr "" msgid "Group" msgstr "" -#: authentication/models/business_account.py:61 +#: authentication/models/business_account.py:62 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" msgstr "" -#: authentication/models/business_account.py:70 +#: authentication/models/business_account.py:72 msgid "Child Business Account" msgstr "" -#: authentication/models/business_account.py:71 +#: authentication/models/business_account.py:73 msgid "Child Business Accounts" msgstr "" -#: authentication/models/business_group.py:8 ml_model/models.py:18 -#: ml_model/models.py:38 ml_model/models.py:63 ml_model/models.py:267 -#: payments/models/payment_plan.py:27 stories/models.py:12 stories/models.py:35 +#: authentication/models/business_group.py:8 ml_model/models.py:15 +#: ml_model/models.py:35 ml_model/models.py:230 +#: payments/models/payment_plan.py:28 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" msgstr "" @@ -127,9 +126,9 @@ msgstr "" msgid "Affiliated by" msgstr "" -#: authentication/models/business_host.py:35 authentication/models/user.py:147 -#: authentication/models/whitelist.py:16 ml_model/models.py:165 -#: payments/models/promocode.py:85 +#: authentication/models/business_host.py:35 authentication/models/user.py:132 +#: authentication/models/whitelist.py:16 ml_model/models.py:125 +#: payments/models/promocode.py:83 msgid "Is active" msgstr "" @@ -137,68 +136,68 @@ msgstr "" msgid "Sector" msgstr "" -#: authentication/models/business_host.py:44 +#: authentication/models/business_host.py:45 msgid "Planned amount of workers" msgstr "" -#: authentication/models/business_host.py:49 +#: authentication/models/business_host.py:51 msgid "Usage intensity" msgstr "" -#: authentication/models/business_host.py:56 +#: authentication/models/business_host.py:58 msgid "Token low balance cap" msgstr "" -#: authentication/models/business_host.py:61 +#: authentication/models/business_host.py:63 msgid "Emails token low balance cap" msgstr "" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:66 msgid "Token low balance cap enabled" msgstr "" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:70 msgid "ITN" msgstr "" -#: authentication/models/business_host.py:67 +#: authentication/models/business_host.py:71 msgid "PSRN" msgstr "" -#: authentication/models/business_host.py:68 ml_model/models.py:178 -#: tools/public_api/models.py:30 +#: authentication/models/business_host.py:73 ml_model/models.py:141 +#: tools/public_api/models.py:31 msgid "Name" msgstr "" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:78 msgid "Preffered name" msgstr "" -#: authentication/models/business_host.py:72 +#: authentication/models/business_host.py:81 msgid "Corporate email" msgstr "" -#: authentication/models/business_host.py:74 +#: authentication/models/business_host.py:84 msgid "Corporate phone" msgstr "" -#: authentication/models/business_host.py:76 +#: authentication/models/business_host.py:87 msgid "Job title" msgstr "" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:93 msgid "Allowed models" msgstr "" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:97 msgid "Log history enabled" msgstr "" -#: authentication/models/business_host.py:103 +#: authentication/models/business_host.py:117 msgid "Business Account" msgstr "" -#: authentication/models/business_host.py:104 +#: authentication/models/business_host.py:118 msgid "Business Accounts" msgstr "" @@ -266,7 +265,7 @@ msgstr "" msgid "Security" msgstr "" -#: authentication/models/email_token.py:15 ml_model/models.py:269 +#: authentication/models/email_token.py:15 ml_model/models.py:234 msgid "Key" msgstr "" @@ -278,43 +277,43 @@ msgstr "" msgid "Email Tokens" msgstr "" -#: authentication/models/user.py:120 authentication/models/user_telegram.py:9 +#: authentication/models/user.py:109 authentication/models/user_telegram.py:10 msgid "First name" msgstr "" -#: authentication/models/user.py:127 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:116 authentication/models/user_telegram.py:13 msgid "Last name" msgstr "" -#: authentication/models/user.py:134 authentication/models/user_telegram.py:11 +#: authentication/models/user.py:119 authentication/models/user_telegram.py:16 msgid "Username" msgstr "" -#: authentication/models/user.py:141 +#: authentication/models/user.py:126 msgid "Email" msgstr "" -#: authentication/models/user.py:149 +#: authentication/models/user.py:134 msgid "Is staff" msgstr "" -#: authentication/models/user.py:150 +#: authentication/models/user.py:135 msgid "Is superuser" msgstr "" -#: authentication/models/user.py:151 +#: authentication/models/user.py:136 msgid "Is email confirmed" msgstr "" -#: authentication/models/user.py:152 +#: authentication/models/user.py:138 msgid "Is subscribed" msgstr "" -#: authentication/models/user.py:158 +#: authentication/models/user.py:145 msgid "Picture name" msgstr "" -#: authentication/models/user.py:167 authentication/models/utm.py:21 +#: authentication/models/user.py:154 authentication/models/utm.py:21 msgid "UTM" msgstr "" @@ -326,38 +325,38 @@ msgstr "" msgid "Is bot" msgstr "" -#: authentication/models/user_telegram.py:12 +#: authentication/models/user_telegram.py:19 msgid "Language" msgstr "" -#: authentication/models/user_telegram.py:13 +#: authentication/models/user_telegram.py:21 msgid "Is premium" msgstr "" -#: authentication/models/user_telegram.py:15 +#: authentication/models/user_telegram.py:23 msgid "Is subscribed to channel" msgstr "" -#: authentication/models/user_telegram.py:25 +#: authentication/models/user_telegram.py:34 msgid "Phonenumber" msgstr "" -#: authentication/models/user_telegram.py:27 +#: authentication/models/user_telegram.py:37 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:10 +#: stories/models.py:15 tools/chats/models.py:11 msgid "Created at" msgstr "" -#: authentication/models/user_telegram.py:28 +#: authentication/models/user_telegram.py:38 #: authentication/models/user_vk.py:15 msgid "Updated at" msgstr "" -#: authentication/models/user_telegram.py:34 +#: authentication/models/user_telegram.py:44 msgid "Telegram User" msgstr "" -#: authentication/models/user_telegram.py:35 +#: authentication/models/user_telegram.py:45 msgid "Telegram Users" msgstr "" @@ -401,376 +400,360 @@ msgstr "" msgid "Whitelists to cancel policies" msgstr "" -#: authentication/selectors/business_host_selector.py:42 -#: authentication/selectors/business_host_selector.py:85 +#: authentication/selectors/business_host_selector.py:38 +#: authentication/selectors/business_host_selector.py:83 msgid "You haven't rights to access host account information" msgstr "" -#: authentication/selectors/business_host_selector.py:60 +#: authentication/selectors/business_host_selector.py:54 msgid "Host user is not registered for this account" msgstr "" -#: authentication/selectors/user_selector.py:80 +#: authentication/selectors/user_selector.py:79 msgid "No user with this uid found" msgstr "" -#: authentication/services/business_account_service.py:58 +#: authentication/services/business_account_service.py:54 msgid "BusinessAccount for this user doesn't exist" msgstr "" -#: authentication/services/business_account_service.py:68 +#: authentication/services/business_account_service.py:65 msgid "Invited account can either accept or reject an invitation" msgstr "" -#: authentication/services/business_account_service.py:73 +#: authentication/services/business_account_service.py:71 msgid "Account is already confirmed" msgstr "" -#: authentication/services/business_host_service.py:152 +#: authentication/services/business_host_service.py:148 msgid "No user_email is provided" msgstr "" -#: authentication/services/business_host_service.py:199 +#: authentication/services/business_host_service.py:203 msgid "No business account by this uid at your company" msgstr "" -#: authentication/services/email_service.py:115 +#: authentication/services/email_service.py:119 msgid "Regular users cannot send introductory letters" msgstr "" -#: authentication/services/email_service.py:137 +#: authentication/services/email_service.py:141 msgid "Regular users cannot send invitation letters" msgstr "" -#: authentication/services/user_services.py:51 +#: authentication/services/user_services.py:49 msgid "New user data is invalid" msgstr "" -#: authentication/services/user_services.py:111 +#: authentication/services/user_services.py:113 msgid "Wrong email" msgstr "" -#: authentication/services/user_services.py:119 backend/urls.py:43 +#: authentication/services/user_services.py:121 backend/urls.py:41 msgid "Wrong password" msgstr "" -#: authentication/services/user_services.py:122 +#: authentication/services/user_services.py:124 msgid "User has not confirmed his email yet" msgstr "" -#: authentication/services/user_services.py:161 +#: authentication/services/user_services.py:163 msgid "No user like this in a database" msgstr "" -#: authentication/services/user_services.py:178 +#: authentication/services/user_services.py:180 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:182 +#: authentication/services/user_services.py:184 msgid "No user token like this in a database" msgstr "" -#: authentication/services/user_services.py:202 +#: authentication/services/user_services.py:204 msgid "No email token provided" msgstr "" -#: authentication/services/user_services.py:206 +#: authentication/services/user_services.py:208 msgid "No token like this in a database" msgstr "" -#: authentication/services/user_services.py:212 +#: authentication/services/user_services.py:217 msgid "Passwords do not match" msgstr "" -#: authentication/services/user_services.py:250 +#: authentication/services/user_services.py:253 msgid "Current password is wrong" msgstr "" -#: backend/urls.py:31 +#: backend/urls.py:28 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:37 +#: backend/urls.py:35 msgid "Token is invalid" msgstr "" -#: backend/urls.py:49 +#: backend/urls.py:47 msgid "Wrong username" msgstr "" -#: messages/serializers.py:42 +#: messages/serializers.py:39 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" msgstr "" -#: ml_model/apps.py:9 ml_model/models.py:146 +#: ml_model/apps.py:8 ml_model/models.py:104 msgid "Neuron Models" msgstr "" -#: ml_model/models.py:29 ml_model/models.py:82 +#: ml_model/models.py:26 ml_model/models.py:46 msgid "Category" msgstr "" -#: ml_model/models.py:30 +#: ml_model/models.py:27 msgid "Categories" msgstr "" #: ml_model/models.py:40 -msgid "Color" -msgstr "" - -#: ml_model/models.py:44 -msgid "Not SVG-pictures not allowed" -msgstr "" - -#: ml_model/models.py:54 -msgid "Model Tag" -msgstr "" - -#: ml_model/models.py:55 -msgid "Model Tags" -msgstr "" - -#: ml_model/models.py:68 -msgid "Alternative Titles" -msgstr "" - -#: ml_model/models.py:74 msgid "Fill automatically, don't touch" msgstr "" -#: ml_model/models.py:90 +#: ml_model/models.py:54 msgid "Avatar" msgstr "" -#: ml_model/models.py:93 -msgid "Tags" -msgstr "" - -#: ml_model/models.py:145 +#: ml_model/models.py:103 msgid "Neuron Model" msgstr "" -#: ml_model/models.py:154 +#: ml_model/models.py:112 msgid "Model" msgstr "" -#: ml_model/models.py:170 ml_model/models.py:171 +#: ml_model/models.py:129 +msgid "Authorization token" +msgstr "" + +#: ml_model/models.py:133 ml_model/models.py:134 msgid "Settings" msgstr "" -#: ml_model/models.py:174 +#: ml_model/models.py:137 #, python-format msgid "Settings of %(model_title)s" msgstr "" -#: ml_model/models.py:193 +#: ml_model/models.py:146 +msgid "Default" +msgstr "" + +#: ml_model/models.py:159 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "" -#: ml_model/models.py:199 +#: ml_model/models.py:165 msgid "Model Version" msgstr "" -#: ml_model/models.py:200 +#: ml_model/models.py:166 msgid "Model Versions" msgstr "" -#: ml_model/models.py:209 +#: ml_model/models.py:175 msgid "Versions" msgstr "" -#: ml_model/models.py:210 +#: ml_model/models.py:176 msgid "Link to versions" msgstr "" -#: ml_model/models.py:219 reports/models/error_report.py:10 +#: ml_model/models.py:185 reports/models/error_report.py:12 msgid "Text" msgstr "" -#: ml_model/models.py:220 stories/models.py:36 +#: ml_model/models.py:186 stories/models.py:36 msgid "Image" msgstr "" -#: ml_model/models.py:221 +#: ml_model/models.py:187 msgid "PDF" msgstr "" -#: ml_model/models.py:222 +#: ml_model/models.py:188 msgid "DOCX" msgstr "" -#: ml_model/models.py:223 +#: ml_model/models.py:189 msgid "DOC" msgstr "" -#: ml_model/models.py:224 +#: ml_model/models.py:190 msgid "Text File (Notebook)" msgstr "" -#: ml_model/models.py:225 +#: ml_model/models.py:191 msgid "ZIP Archive" msgstr "" -#: ml_model/models.py:226 +#: ml_model/models.py:192 msgid "Audio" msgstr "" -#: ml_model/models.py:232 ml_model/models.py:271 +#: ml_model/models.py:198 ml_model/models.py:236 #: payments/models/promocode.py:41 msgid "Type" msgstr "" -#: ml_model/models.py:234 ml_model/models.py:282 +#: ml_model/models.py:200 ml_model/models.py:249 msgid "Required" msgstr "" -#: ml_model/models.py:237 +#: ml_model/models.py:203 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "" -#: ml_model/models.py:243 +#: ml_model/models.py:209 msgid "Model Input" msgstr "" -#: ml_model/models.py:244 +#: ml_model/models.py:210 msgid "Model Inputs" msgstr "" -#: ml_model/models.py:250 +#: ml_model/models.py:216 msgid "Integer" msgstr "" -#: ml_model/models.py:251 +#: ml_model/models.py:217 msgid "Float" msgstr "" -#: ml_model/models.py:252 +#: ml_model/models.py:218 msgid "String" msgstr "" -#: ml_model/models.py:255 +#: ml_model/models.py:219 msgid "List" msgstr "" -#: ml_model/models.py:259 +#: ml_model/models.py:222 msgid "Float range" msgstr "" -#: ml_model/models.py:263 +#: ml_model/models.py:226 msgid "Integer range" msgstr "" -#: ml_model/models.py:265 +#: ml_model/models.py:228 msgid "Logical" msgstr "" -#: ml_model/models.py:278 +#: ml_model/models.py:243 msgid "Values" msgstr "" -#: ml_model/models.py:279 +#: ml_model/models.py:245 msgid "" "These values can contain different interfaces and default value optional" msgstr "" -#: ml_model/models.py:281 +#: ml_model/models.py:248 msgid "Hidden" msgstr "" -#: ml_model/models.py:287 +#: ml_model/models.py:254 #, python-format msgid "Parameter of %(model_title)s" msgstr "" -#: ml_model/models.py:290 +#: ml_model/models.py:257 msgid "Parameter" msgstr "" -#: ml_model/models.py:291 +#: ml_model/models.py:258 msgid "Parameters" msgstr "" -#: ml_model/models.py:296 +#: ml_model/models.py:263 msgid "Fixed" msgstr "" -#: ml_model/models.py:297 +#: ml_model/models.py:264 msgid "Per generation second" msgstr "" -#: ml_model/models.py:298 +#: ml_model/models.py:265 msgid "Per one text token" msgstr "" -#: ml_model/models.py:299 +#: ml_model/models.py:266 msgid "Per image pixel" msgstr "" -#: ml_model/models.py:302 +#: ml_model/models.py:269 msgid "By input data" msgstr "" -#: ml_model/models.py:303 +#: ml_model/models.py:270 msgid "By output data" msgstr "" -#: ml_model/models.py:304 +#: ml_model/models.py:271 msgid "By all data" msgstr "" -#: ml_model/models.py:309 +#: ml_model/models.py:274 msgid "Strategy" msgstr "" -#: ml_model/models.py:314 +#: ml_model/models.py:279 msgid "Interaction Type" msgstr "" -#: ml_model/models.py:319 payments/models/invoice.py:19 +#: ml_model/models.py:284 payments/models/invoice.py:19 msgid "Cost" msgstr "" -#: ml_model/models.py:320 +#: ml_model/models.py:285 msgid "In RUB, per specified strategy" msgstr "" -#: ml_model/models.py:325 +#: ml_model/models.py:290 msgid "Coefficient" msgstr "" -#: ml_model/models.py:326 +#: ml_model/models.py:291 msgid "Cost multiplier" msgstr "" -#: ml_model/models.py:333 +#: ml_model/models.py:298 msgid "Rate" msgstr "" -#: ml_model/models.py:337 +#: ml_model/models.py:302 msgid "Payment Rule" msgstr "" -#: ml_model/models.py:338 +#: ml_model/models.py:303 msgid "Payment Rules" msgstr "" -#: ml_model/selectors/ml_models_selector.py:75 +#: ml_model/selectors/ml_models_selector.py:79 msgid "no model by this id" msgstr "" #: ml_model/services/minio_service.py:35 ml_model/services/minio_service.py:53 -#: ml_model/services/minio_service.py:61 ml_model/services/minio_service.py:70 +#: ml_model/services/minio_service.py:65 ml_model/services/minio_service.py:74 msgid "Unknown bucket destination" msgstr "" -#: ml_model/services/upscaleai.py:124 +#: ml_model/services/upscaleai.py:123 msgid "No image given for improving" msgstr "" -#: payments/apps.py:9 payments/models/payment.py:60 +#: payments/apps.py:9 payments/models/payment.py:62 msgid "Payments" msgstr "" @@ -806,7 +789,7 @@ msgstr "" msgid "Status" msgstr "" -#: payments/models/payment.py:59 +#: payments/models/payment.py:61 msgid "Payment" msgstr "" @@ -826,87 +809,87 @@ msgstr "" msgid "Is recurrent" msgstr "" -#: payments/models/payment_plan.py:29 +#: payments/models/payment_plan.py:31 msgid "Duration" msgstr "" -#: payments/models/payment_plan.py:34 +#: payments/models/payment_plan.py:36 msgid "Is visible" msgstr "" -#: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 +#: payments/models/payment_plan.py:54 payments/models/payment_plan.py:69 msgid "Payment Plan" msgstr "" -#: payments/models/payment_plan.py:53 +#: payments/models/payment_plan.py:55 msgid "Payment Plans" msgstr "" -#: payments/models/payment_plan.py:69 +#: payments/models/payment_plan.py:71 msgid "Last payment at" msgstr "" -#: payments/models/payment_plan.py:70 +#: payments/models/payment_plan.py:72 msgid "Next payment at" msgstr "" -#: payments/models/payment_plan.py:72 +#: payments/models/payment_plan.py:74 msgid "Current balance" msgstr "" -#: payments/models/payment_plan.py:78 +#: payments/models/payment_plan.py:80 msgid "Recurrent billing task" msgstr "" -#: payments/models/payment_plan.py:99 payments/models/payment_plan.py:100 +#: payments/models/payment_plan.py:97 payments/models/payment_plan.py:98 msgid "User Balance" msgstr "" -#: payments/models/promocode.py:48 +#: payments/models/promocode.py:45 msgid "Action Function" msgstr "" -#: payments/models/promocode.py:73 +#: payments/models/promocode.py:70 msgid "Can be only for referral promos" msgstr "" -#: payments/models/promocode.py:81 +#: payments/models/promocode.py:78 msgid "Is personal" msgstr "" -#: payments/models/promocode.py:82 +#: payments/models/promocode.py:79 msgid "Can be activated only one time" msgstr "" -#: payments/models/promocode.py:89 +#: payments/models/promocode.py:88 msgid "Owner can be only for refferal promos" msgstr "" -#: payments/models/promocode.py:97 payments/models/promocode.py:107 +#: payments/models/promocode.py:96 payments/models/promocode.py:107 msgid "Promocode" msgstr "" -#: payments/models/promocode.py:98 +#: payments/models/promocode.py:97 msgid "Promocodes" msgstr "" -#: payments/models/promocode.py:105 +#: payments/models/promocode.py:104 msgid "Activated by" msgstr "" -#: payments/models/promocode.py:137 +#: payments/models/promocode.py:138 msgid "Promocode Activation" msgstr "" -#: payments/models/promocode.py:138 +#: payments/models/promocode.py:139 msgid "Promocode Activations" msgstr "" -#: payments/models/user_payment_method.py:24 +#: payments/models/user_payment_method.py:28 msgid "Payment Method" msgstr "" -#: payments/models/user_payment_method.py:25 +#: payments/models/user_payment_method.py:29 msgid "Payment Methods" msgstr "" @@ -914,15 +897,15 @@ msgstr "" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:32 +#: payments/selectors/payment_plan_selector.py:29 msgid "Business accounts are not allowed to make purchases" msgstr "" -#: payments/selectors/payment_plan_selector.py:57 +#: payments/selectors/payment_plan_selector.py:54 msgid "No plan by this uid" msgstr "" -#: payments/services/model_billing_service.py:31 +#: payments/services/model_billing_service.py:27 msgid "Unknown account type" msgstr "" @@ -950,19 +933,19 @@ msgstr "" msgid "Proxies" msgstr "" -#: reports/models/error_report.py:9 +#: reports/models/error_report.py:10 msgid "Author" msgstr "" -#: reports/models/error_report.py:11 +#: reports/models/error_report.py:13 msgid "Attachments" msgstr "" -#: reports/models/error_report.py:14 +#: reports/models/error_report.py:16 msgid "User Report" msgstr "" -#: reports/models/error_report.py:15 +#: reports/models/error_report.py:17 msgid "User Reports" msgstr "" @@ -1006,7 +989,7 @@ msgstr "" msgid "Tools" msgstr "" -#: tools/apps.py:15 tools/chats/models.py:21 +#: tools/apps.py:15 tools/chats/models.py:23 msgid "Chats" msgstr "" @@ -1026,16 +1009,16 @@ msgstr "" msgid "Feed" msgstr "" -#: tools/chats/models.py:13 tools/public_api/models.py:45 +#: tools/chats/models.py:15 tools/public_api/models.py:47 msgid "Is deleted" msgstr "" -#: tools/chats/models.py:17 +#: tools/chats/models.py:19 #, python-format msgid "Chat %(id)s" msgstr "" -#: tools/chats/models.py:20 +#: tools/chats/models.py:22 msgid "Chat" msgstr "" @@ -1047,18 +1030,14 @@ msgstr "" msgid "API Key not found" msgstr "" -#: tools/public_api/models.py:46 +#: tools/public_api/models.py:48 msgid "Expires at" msgstr "" -#: tools/public_api/models.py:50 +#: tools/public_api/models.py:52 msgid "API Key" msgstr "" -#: tools/public_api/models.py:51 +#: tools/public_api/models.py:53 msgid "API Keys" msgstr "" - -#: tools/public_api/views/base.py:54 -msgid "Model is blocked by outdating or temporary block, please retry later" -msgstr "" @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-30 03:00+0300\n" +"POT-Creation-Date: 2025-03-24 18:01+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,8 +20,7 @@ msgstr "" "n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " "(n%100>=11 && n%100<=14)? 2 : 3);\n" -#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:47 -#: stories/models.py:18 +#: achievements/admin.py:11 achievements/models.py:19 stories/models.py:18 msgid "Icon" msgstr "Миниатюра" @@ -33,21 +32,21 @@ msgstr "Достижение" msgid "Achievements" msgstr "Достижения" -#: achievements/models.py:14 ml_model/models.py:19 ml_model/models.py:39 -#: ml_model/models.py:72 ml_model/models.py:180 +#: achievements/models.py:14 ml_model/models.py:16 ml_model/models.py:38 +#: ml_model/models.py:145 msgid "Slug" msgstr "Ярлык" -#: achievements/models.py:16 ml_model/models.py:70 ml_model/models.py:179 -#: ml_model/models.py:268 payments/models/payment.py:52 +#: achievements/models.py:16 ml_model/models.py:36 ml_model/models.py:143 +#: ml_model/models.py:232 payments/models/payment.py:53 msgid "Description" msgstr "Описание" #: achievements/models.py:43 authentication/models/business_host.py:21 -#: authentication/models/email_token.py:12 authentication/models/user.py:241 -#: authentication/models/user.py:242 authentication/models/user_telegram.py:22 +#: authentication/models/email_token.py:12 authentication/models/user.py:229 +#: authentication/models/user.py:230 authentication/models/user_telegram.py:30 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:61 +#: payments/models/payment.py:26 payments/models/payment_plan.py:63 msgid "User" msgstr "Пользователь" @@ -65,8 +64,8 @@ msgstr "Достижение %(achievement_title)s пользователя %(us msgid "Issued achievement" msgstr "Выданное достижение" -#: authentication/models/business_account.py:16 payments/models/promocode.py:72 -#: tools/public_api/models.py:33 +#: authentication/models/business_account.py:16 payments/models/promocode.py:69 +#: tools/public_api/models.py:35 msgid "Owner" msgstr "Владелец" @@ -89,7 +88,7 @@ msgid "Acceptance" msgstr "Подтверждение" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:43 +#: authentication/models/business_group.py:21 tools/public_api/models.py:45 msgid "Token limit" msgstr "Лимит токенов" @@ -97,7 +96,7 @@ msgstr "Лимит токенов" msgid "Group" msgstr "Группа" -#: authentication/models/business_account.py:61 +#: authentication/models/business_account.py:62 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" @@ -105,17 +104,17 @@ msgstr "" "Невозможно добавить сотрудника к группе, когда он не принадлежит данной " "компании" -#: authentication/models/business_account.py:70 +#: authentication/models/business_account.py:72 msgid "Child Business Account" msgstr "Дочерний Бизнес Аккаунт" -#: authentication/models/business_account.py:71 +#: authentication/models/business_account.py:73 msgid "Child Business Accounts" msgstr "Дочерние Бизнес Аккаунты" -#: authentication/models/business_group.py:8 ml_model/models.py:18 -#: ml_model/models.py:38 ml_model/models.py:63 ml_model/models.py:267 -#: payments/models/payment_plan.py:27 stories/models.py:12 stories/models.py:35 +#: authentication/models/business_group.py:8 ml_model/models.py:15 +#: ml_model/models.py:35 ml_model/models.py:230 +#: payments/models/payment_plan.py:28 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" msgstr "Название" @@ -132,9 +131,9 @@ msgstr "Бизнес Группы" msgid "Affiliated by" msgstr "Кем привлечена" -#: authentication/models/business_host.py:35 authentication/models/user.py:147 -#: authentication/models/whitelist.py:16 ml_model/models.py:165 -#: payments/models/promocode.py:85 +#: authentication/models/business_host.py:35 authentication/models/user.py:132 +#: authentication/models/whitelist.py:16 ml_model/models.py:125 +#: payments/models/promocode.py:83 msgid "Is active" msgstr "Является активной" @@ -142,68 +141,68 @@ msgstr "Является активной" msgid "Sector" msgstr "Сектор" -#: authentication/models/business_host.py:44 +#: authentication/models/business_host.py:45 msgid "Planned amount of workers" msgstr "Планируемое число сотрудников" -#: authentication/models/business_host.py:49 +#: authentication/models/business_host.py:51 msgid "Usage intensity" msgstr "Частота использования" -#: authentication/models/business_host.py:56 +#: authentication/models/business_host.py:58 msgid "Token low balance cap" msgstr "Предел низкого баланса" -#: authentication/models/business_host.py:61 +#: authentication/models/business_host.py:63 msgid "Emails token low balance cap" msgstr "Email'ы для рассылки по низкому балансу" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:66 msgid "Token low balance cap enabled" msgstr "Рассылка по низкому балансу включена" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:70 msgid "ITN" msgstr "ИНН" -#: authentication/models/business_host.py:67 +#: authentication/models/business_host.py:71 msgid "PSRN" msgstr "ОГРН" -#: authentication/models/business_host.py:68 ml_model/models.py:178 -#: tools/public_api/models.py:30 +#: authentication/models/business_host.py:73 ml_model/models.py:141 +#: tools/public_api/models.py:31 msgid "Name" msgstr "Наименование" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:78 msgid "Preffered name" msgstr "" -#: authentication/models/business_host.py:72 +#: authentication/models/business_host.py:81 msgid "Corporate email" msgstr "Корпоративная почта" -#: authentication/models/business_host.py:74 +#: authentication/models/business_host.py:84 msgid "Corporate phone" msgstr "Корпоративный телефон" -#: authentication/models/business_host.py:76 +#: authentication/models/business_host.py:87 msgid "Job title" msgstr "Наименование работ" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:93 msgid "Allowed models" msgstr "Разрешенные модели" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:97 msgid "Log history enabled" msgstr "История логов включена" -#: authentication/models/business_host.py:103 +#: authentication/models/business_host.py:117 msgid "Business Account" msgstr "Бизнес Аккаунт" -#: authentication/models/business_host.py:104 +#: authentication/models/business_host.py:118 msgid "Business Accounts" msgstr "Бизнес Аккаунты" @@ -271,7 +270,7 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:15 ml_model/models.py:269 +#: authentication/models/email_token.py:15 ml_model/models.py:234 msgid "Key" msgstr "Ключ" @@ -283,43 +282,43 @@ msgstr "Email Токен" msgid "Email Tokens" msgstr "Email Токены" -#: authentication/models/user.py:120 authentication/models/user_telegram.py:9 +#: authentication/models/user.py:109 authentication/models/user_telegram.py:10 msgid "First name" msgstr "Имя" -#: authentication/models/user.py:127 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:116 authentication/models/user_telegram.py:13 msgid "Last name" msgstr "Фамилия" -#: authentication/models/user.py:134 authentication/models/user_telegram.py:11 +#: authentication/models/user.py:119 authentication/models/user_telegram.py:16 msgid "Username" msgstr "Имя пользователя" -#: authentication/models/user.py:141 +#: authentication/models/user.py:126 msgid "Email" msgstr "Email" -#: authentication/models/user.py:149 +#: authentication/models/user.py:134 msgid "Is staff" msgstr "Административный" -#: authentication/models/user.py:150 +#: authentication/models/user.py:135 msgid "Is superuser" msgstr "Суперюзер" -#: authentication/models/user.py:151 +#: authentication/models/user.py:136 msgid "Is email confirmed" msgstr "Email подтвержден" -#: authentication/models/user.py:152 +#: authentication/models/user.py:138 msgid "Is subscribed" msgstr "Подписан на уведомления" -#: authentication/models/user.py:158 +#: authentication/models/user.py:145 msgid "Picture name" msgstr "Имя аватара" -#: authentication/models/user.py:167 authentication/models/utm.py:21 +#: authentication/models/user.py:154 authentication/models/utm.py:21 msgid "UTM" msgstr "UTM" @@ -331,38 +330,38 @@ msgstr "Телеграм ID" msgid "Is bot" msgstr "Является ботом" -#: authentication/models/user_telegram.py:12 +#: authentication/models/user_telegram.py:19 msgid "Language" msgstr "Язык" -#: authentication/models/user_telegram.py:13 +#: authentication/models/user_telegram.py:21 msgid "Is premium" msgstr "Премиум" -#: authentication/models/user_telegram.py:15 +#: authentication/models/user_telegram.py:23 msgid "Is subscribed to channel" msgstr "Подписан на канал" -#: authentication/models/user_telegram.py:25 +#: authentication/models/user_telegram.py:34 msgid "Phonenumber" msgstr "Номер телефона" -#: authentication/models/user_telegram.py:27 +#: authentication/models/user_telegram.py:37 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:10 +#: stories/models.py:15 tools/chats/models.py:11 msgid "Created at" msgstr "Когда создан" -#: authentication/models/user_telegram.py:28 +#: authentication/models/user_telegram.py:38 #: authentication/models/user_vk.py:15 msgid "Updated at" msgstr "Когда обновлен" -#: authentication/models/user_telegram.py:34 +#: authentication/models/user_telegram.py:44 msgid "Telegram User" msgstr "Пользователь телеграм" -#: authentication/models/user_telegram.py:35 +#: authentication/models/user_telegram.py:45 msgid "Telegram Users" msgstr "Пользователи Телеграм" @@ -406,388 +405,374 @@ msgstr "Вайтлист для отмены политик" msgid "Whitelists to cancel policies" msgstr "Вайтлисты для отмены политик" -#: authentication/selectors/business_host_selector.py:42 -#: authentication/selectors/business_host_selector.py:85 +#: authentication/selectors/business_host_selector.py:38 +#: authentication/selectors/business_host_selector.py:83 msgid "You haven't rights to access host account information" msgstr "" -#: authentication/selectors/business_host_selector.py:60 +#: authentication/selectors/business_host_selector.py:54 msgid "Host user is not registered for this account" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" -#: authentication/selectors/user_selector.py:80 +#: authentication/selectors/user_selector.py:79 msgid "No user with this uid found" msgstr "Не найден пользователь с данным ID" -#: authentication/services/business_account_service.py:58 +#: authentication/services/business_account_service.py:54 msgid "BusinessAccount for this user doesn't exist" msgstr "Бизнес-аккаунт для данного юзера не найден" -#: authentication/services/business_account_service.py:68 +#: authentication/services/business_account_service.py:65 msgid "Invited account can either accept or reject an invitation" msgstr "Приглашенный аккаунт может принять или отклонить приглашение" -#: authentication/services/business_account_service.py:73 +#: authentication/services/business_account_service.py:71 msgid "Account is already confirmed" msgstr "Аккаунт уже подтвержден" -#: authentication/services/business_host_service.py:152 +#: authentication/services/business_host_service.py:148 msgid "No user_email is provided" msgstr "" -#: authentication/services/business_host_service.py:199 +#: authentication/services/business_host_service.py:203 msgid "No business account by this uid at your company" msgstr "Такого аккаунта нет в вашей компании" -#: authentication/services/email_service.py:115 +#: authentication/services/email_service.py:119 msgid "Regular users cannot send introductory letters" msgstr "Обычные пользователи не могут отсылать письма" -#: authentication/services/email_service.py:137 +#: authentication/services/email_service.py:141 msgid "Regular users cannot send invitation letters" msgstr "Обычные пользователи не могут отправлять письма для приглашений" -#: authentication/services/user_services.py:51 +#: authentication/services/user_services.py:49 msgid "New user data is invalid" msgstr "" -#: authentication/services/user_services.py:111 +#: authentication/services/user_services.py:113 msgid "Wrong email" msgstr "Неверный email" -#: authentication/services/user_services.py:119 backend/urls.py:43 +#: authentication/services/user_services.py:121 backend/urls.py:41 msgid "Wrong password" msgstr "Неверный пароль" -#: authentication/services/user_services.py:122 +#: authentication/services/user_services.py:124 msgid "User has not confirmed his email yet" msgstr "Пользователь пока не подтвердил свой email" -#: authentication/services/user_services.py:161 +#: authentication/services/user_services.py:163 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:178 +#: authentication/services/user_services.py:180 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:182 +#: authentication/services/user_services.py:184 msgid "No user token like this in a database" msgstr "" -#: authentication/services/user_services.py:202 +#: authentication/services/user_services.py:204 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:206 +#: authentication/services/user_services.py:208 msgid "No token like this in a database" msgstr "Не найдено такого токена" -#: authentication/services/user_services.py:212 +#: authentication/services/user_services.py:217 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:250 +#: authentication/services/user_services.py:253 msgid "Current password is wrong" msgstr "Текущий пароль неверен" -#: backend/urls.py:31 +#: backend/urls.py:28 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:37 +#: backend/urls.py:35 msgid "Token is invalid" msgstr "" -#: backend/urls.py:49 +#: backend/urls.py:47 #, fuzzy #| msgid "Wrong email" msgid "Wrong username" msgstr "Неверный email" -#: messages/serializers.py:42 +#: messages/serializers.py:39 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" -msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" +msgstr "" -#: ml_model/apps.py:9 ml_model/models.py:146 +#: ml_model/apps.py:8 ml_model/models.py:104 msgid "Neuron Models" msgstr "Нейронные Модели" -#: ml_model/models.py:29 ml_model/models.py:82 +#: ml_model/models.py:26 ml_model/models.py:46 msgid "Category" msgstr "Категория" -#: ml_model/models.py:30 +#: ml_model/models.py:27 msgid "Categories" msgstr "Категории" #: ml_model/models.py:40 -msgid "Color" -msgstr "Цвет" - -#: ml_model/models.py:44 -msgid "Not SVG-pictures not allowed" -msgstr "Нельзя использовать не SVG-картинки" - -#: ml_model/models.py:54 -msgid "Model Tag" -msgstr "Тег модели" - -#: ml_model/models.py:55 -msgid "Model Tags" -msgstr "Теги модели" - -#: ml_model/models.py:68 -msgid "Alternative Titles" -msgstr "Альтернативные названия" - -#: ml_model/models.py:74 msgid "Fill automatically, don't touch" msgstr "Заполняется автоматически, не трогать" -#: ml_model/models.py:90 +#: ml_model/models.py:54 msgid "Avatar" msgstr "Аватар" -#: ml_model/models.py:93 -msgid "Tags" -msgstr "Теги" - -#: ml_model/models.py:145 +#: ml_model/models.py:103 msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:154 +#: ml_model/models.py:112 msgid "Model" msgstr "Модель" -#: ml_model/models.py:170 ml_model/models.py:171 +#: ml_model/models.py:129 +msgid "Authorization token" +msgstr "Авторизационный токен" + +#: ml_model/models.py:133 ml_model/models.py:134 msgid "Settings" msgstr "Настройки" -#: ml_model/models.py:174 +#: ml_model/models.py:137 #, fuzzy, python-format #| msgid "Settings of %(model_title)" msgid "Settings of %(model_title)s" msgstr "Настройки %(model_title)s" -#: ml_model/models.py:193 +#: ml_model/models.py:146 +#, fuzzy +#| msgid "Default value" +msgid "Default" +msgstr "Стандартное значение" + +#: ml_model/models.py:159 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "%(model_title)s | %(version_name)s" -#: ml_model/models.py:199 +#: ml_model/models.py:165 msgid "Model Version" msgstr "Версия Модели" -#: ml_model/models.py:200 +#: ml_model/models.py:166 msgid "Model Versions" msgstr "Версии Модели" -#: ml_model/models.py:209 +#: ml_model/models.py:175 msgid "Versions" msgstr "Версии" -#: ml_model/models.py:210 +#: ml_model/models.py:176 msgid "Link to versions" msgstr "Привязка к версиям" -#: ml_model/models.py:219 reports/models/error_report.py:10 +#: ml_model/models.py:185 reports/models/error_report.py:12 msgid "Text" msgstr "Текст" -#: ml_model/models.py:220 stories/models.py:36 +#: ml_model/models.py:186 stories/models.py:36 msgid "Image" msgstr "Картинка" -#: ml_model/models.py:221 +#: ml_model/models.py:187 msgid "PDF" msgstr "PDF" -#: ml_model/models.py:222 +#: ml_model/models.py:188 msgid "DOCX" -msgstr "DOCX" +msgstr "" -#: ml_model/models.py:223 +#: ml_model/models.py:189 msgid "DOC" -msgstr "DOC" +msgstr "" -#: ml_model/models.py:224 +#: ml_model/models.py:190 msgid "Text File (Notebook)" msgstr "Текстовый файл (Блокнот)" -#: ml_model/models.py:225 +#: ml_model/models.py:191 msgid "ZIP Archive" msgstr "ZIP архив" -#: ml_model/models.py:226 +#: ml_model/models.py:192 msgid "Audio" msgstr "Аудио" -#: ml_model/models.py:232 ml_model/models.py:271 +#: ml_model/models.py:198 ml_model/models.py:236 #: payments/models/promocode.py:41 msgid "Type" msgstr "Тип" -#: ml_model/models.py:234 ml_model/models.py:282 +#: ml_model/models.py:200 ml_model/models.py:249 msgid "Required" msgstr "Обязательный" -#: ml_model/models.py:237 +#: ml_model/models.py:203 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "%(model_title)s | %(input_type)s" -#: ml_model/models.py:243 +#: ml_model/models.py:209 #, fuzzy #| msgid "Model" msgid "Model Input" msgstr "Модель" -#: ml_model/models.py:244 +#: ml_model/models.py:210 msgid "Model Inputs" msgstr "Входящий поток модели" -#: ml_model/models.py:250 +#: ml_model/models.py:216 msgid "Integer" msgstr "Целое число" -#: ml_model/models.py:251 +#: ml_model/models.py:217 msgid "Float" msgstr "Вещественное число" -#: ml_model/models.py:252 +#: ml_model/models.py:218 msgid "String" msgstr "Строка" -#: ml_model/models.py:255 +#: ml_model/models.py:219 msgid "List" msgstr "Список" -#: ml_model/models.py:259 +#: ml_model/models.py:222 msgid "Float range" msgstr "Вещественный диапазон" -#: ml_model/models.py:263 +#: ml_model/models.py:226 msgid "Integer range" msgstr "Целочисленный диапазон" -#: ml_model/models.py:265 +#: ml_model/models.py:228 msgid "Logical" msgstr "Логический" -#: ml_model/models.py:278 +#: ml_model/models.py:243 msgid "Values" msgstr "Значения" -#: ml_model/models.py:279 +#: ml_model/models.py:245 msgid "" "These values can contain different interfaces and default value optional" msgstr "" "Значения могут содержать различные интерфейс и, опционально, значение по " "умолчанию" -#: ml_model/models.py:281 +#: ml_model/models.py:248 msgid "Hidden" msgstr "Скрытый" -#: ml_model/models.py:287 +#: ml_model/models.py:254 #, fuzzy, python-format #| msgid "Parameter of %(model_title)" msgid "Parameter of %(model_title)s" msgstr "Параметр %(model_title)s" -#: ml_model/models.py:290 +#: ml_model/models.py:257 msgid "Parameter" msgstr "Параметр" -#: ml_model/models.py:291 +#: ml_model/models.py:258 msgid "Parameters" msgstr "Параметры" -#: ml_model/models.py:296 +#: ml_model/models.py:263 msgid "Fixed" msgstr "Фикса" -#: ml_model/models.py:297 +#: ml_model/models.py:264 msgid "Per generation second" msgstr "За секунду генерации" -#: ml_model/models.py:298 +#: ml_model/models.py:265 msgid "Per one text token" msgstr "За один текстовый токен" -#: ml_model/models.py:299 +#: ml_model/models.py:266 msgid "Per image pixel" msgstr "За один пиксель" -#: ml_model/models.py:302 +#: ml_model/models.py:269 msgid "By input data" msgstr "По входящим данным" -#: ml_model/models.py:303 +#: ml_model/models.py:270 msgid "By output data" msgstr "По исходящим данным" -#: ml_model/models.py:304 +#: ml_model/models.py:271 msgid "By all data" msgstr "По всем данным" -#: ml_model/models.py:309 +#: ml_model/models.py:274 #, fuzzy #| msgid "Category" msgid "Strategy" msgstr "Стратегия" -#: ml_model/models.py:314 +#: ml_model/models.py:279 msgid "Interaction Type" msgstr "Тип взаимодействия" -#: ml_model/models.py:319 payments/models/invoice.py:19 +#: ml_model/models.py:284 payments/models/invoice.py:19 msgid "Cost" msgstr "Цена" -#: ml_model/models.py:320 +#: ml_model/models.py:285 msgid "In RUB, per specified strategy" msgstr "В рублях, за указанную стратегию" -#: ml_model/models.py:325 +#: ml_model/models.py:290 msgid "Coefficient" msgstr "Коэффициент" -#: ml_model/models.py:326 +#: ml_model/models.py:291 msgid "Cost multiplier" msgstr "Цена" -#: ml_model/models.py:333 +#: ml_model/models.py:298 msgid "Rate" msgstr "Ставка" -#: ml_model/models.py:337 +#: ml_model/models.py:302 #, fuzzy #| msgid "Payment Plan" msgid "Payment Rule" msgstr "Платежное правило" -#: ml_model/models.py:338 +#: ml_model/models.py:303 msgid "Payment Rules" msgstr "Платежные правила" -#: ml_model/selectors/ml_models_selector.py:75 +#: ml_model/selectors/ml_models_selector.py:79 msgid "no model by this id" msgstr "Не найдено моделей по этому ID" #: ml_model/services/minio_service.py:35 ml_model/services/minio_service.py:53 -#: ml_model/services/minio_service.py:61 ml_model/services/minio_service.py:70 +#: ml_model/services/minio_service.py:65 ml_model/services/minio_service.py:74 msgid "Unknown bucket destination" msgstr "Неизвестный бакет для загрузки" -#: ml_model/services/upscaleai.py:124 +#: ml_model/services/upscaleai.py:123 msgid "No image given for improving" msgstr "Нет изображения для улучшения" -#: payments/apps.py:9 payments/models/payment.py:60 +#: payments/apps.py:9 payments/models/payment.py:62 msgid "Payments" msgstr "Платежи" @@ -826,7 +811,7 @@ msgstr "План" msgid "Status" msgstr "Статус" -#: payments/models/payment.py:59 +#: payments/models/payment.py:61 msgid "Payment" msgstr "Платеж" @@ -846,89 +831,89 @@ msgstr "Корпоративный" msgid "Is recurrent" msgstr "Рекуррентный" -#: payments/models/payment_plan.py:29 +#: payments/models/payment_plan.py:31 msgid "Duration" msgstr "Длительность" -#: payments/models/payment_plan.py:34 +#: payments/models/payment_plan.py:36 msgid "Is visible" msgstr "Видимый" -#: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 +#: payments/models/payment_plan.py:54 payments/models/payment_plan.py:69 msgid "Payment Plan" msgstr "Платежный План" -#: payments/models/payment_plan.py:53 +#: payments/models/payment_plan.py:55 msgid "Payment Plans" msgstr "Платежные Планы" -#: payments/models/payment_plan.py:69 +#: payments/models/payment_plan.py:71 msgid "Last payment at" msgstr "Последнее время платежа" -#: payments/models/payment_plan.py:70 +#: payments/models/payment_plan.py:72 msgid "Next payment at" msgstr "Следующее время платежа" -#: payments/models/payment_plan.py:72 +#: payments/models/payment_plan.py:74 msgid "Current balance" msgstr "Текущий баланс" -#: payments/models/payment_plan.py:78 +#: payments/models/payment_plan.py:80 msgid "Recurrent billing task" msgstr "Рекуррентная задача на платеж" -#: payments/models/payment_plan.py:99 payments/models/payment_plan.py:100 +#: payments/models/payment_plan.py:97 payments/models/payment_plan.py:98 msgid "User Balance" msgstr "Баланс пользователя" -#: payments/models/promocode.py:48 +#: payments/models/promocode.py:45 msgid "Action Function" msgstr "Активирующаяся функция" -#: payments/models/promocode.py:73 +#: payments/models/promocode.py:70 msgid "Can be only for referral promos" msgstr "Может быть только у реферальных промокодов" -#: payments/models/promocode.py:81 +#: payments/models/promocode.py:78 msgid "Is personal" msgstr "Персональный" -#: payments/models/promocode.py:82 +#: payments/models/promocode.py:79 msgid "Can be activated only one time" msgstr "Может быть активирован только один раз" -#: payments/models/promocode.py:89 +#: payments/models/promocode.py:88 #, fuzzy #| msgid "Owner can be only for referral promos" msgid "Owner can be only for refferal promos" msgstr "Владелец может быть только у реферальных промокодов" -#: payments/models/promocode.py:97 payments/models/promocode.py:107 +#: payments/models/promocode.py:96 payments/models/promocode.py:107 msgid "Promocode" msgstr "Промокод" -#: payments/models/promocode.py:98 +#: payments/models/promocode.py:97 msgid "Promocodes" msgstr "Промокоды" -#: payments/models/promocode.py:105 +#: payments/models/promocode.py:104 msgid "Activated by" msgstr "Кем активирован" -#: payments/models/promocode.py:137 +#: payments/models/promocode.py:138 msgid "Promocode Activation" msgstr "Активация Промокода" -#: payments/models/promocode.py:138 +#: payments/models/promocode.py:139 msgid "Promocode Activations" msgstr "Активации Промокодов" -#: payments/models/user_payment_method.py:24 +#: payments/models/user_payment_method.py:28 msgid "Payment Method" msgstr "Платежный метод" -#: payments/models/user_payment_method.py:25 +#: payments/models/user_payment_method.py:29 msgid "Payment Methods" msgstr "Платежные методы" @@ -936,15 +921,15 @@ msgstr "Платежные методы" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:32 +#: payments/selectors/payment_plan_selector.py:29 msgid "Business accounts are not allowed to make purchases" msgstr "Сотрудники не могут производить покупки" -#: payments/selectors/payment_plan_selector.py:57 +#: payments/selectors/payment_plan_selector.py:54 msgid "No plan by this uid" msgstr "Не найдено подписки по этому ID" -#: payments/services/model_billing_service.py:31 +#: payments/services/model_billing_service.py:27 msgid "Unknown account type" msgstr "Неизвестный тип аккаунта" @@ -972,19 +957,19 @@ msgstr "Прокси" msgid "Proxies" msgstr "Прокси" -#: reports/models/error_report.py:9 +#: reports/models/error_report.py:10 msgid "Author" msgstr "Автор" -#: reports/models/error_report.py:11 +#: reports/models/error_report.py:13 msgid "Attachments" msgstr "Вложения" -#: reports/models/error_report.py:14 +#: reports/models/error_report.py:16 msgid "User Report" msgstr "Пользовательский репорт" -#: reports/models/error_report.py:15 +#: reports/models/error_report.py:17 msgid "User Reports" msgstr "Пользовательские репорты" @@ -1028,7 +1013,7 @@ msgstr "Виджеты" msgid "Tools" msgstr "Инструменты" -#: tools/apps.py:15 tools/chats/models.py:21 +#: tools/apps.py:15 tools/chats/models.py:23 msgid "Chats" msgstr "Чаты" @@ -1048,17 +1033,17 @@ msgstr "Медиа" msgid "Feed" msgstr "Шейр пользователей" -#: tools/chats/models.py:13 tools/public_api/models.py:45 +#: tools/chats/models.py:15 tools/public_api/models.py:47 msgid "Is deleted" msgstr "Удален" -#: tools/chats/models.py:17 +#: tools/chats/models.py:19 #, fuzzy, python-format #| msgid "Chat %(id)" msgid "Chat %(id)s" msgstr "Чат %(id)s" -#: tools/chats/models.py:20 +#: tools/chats/models.py:22 msgid "Chat" msgstr "Чат" @@ -1070,30 +1055,18 @@ msgstr "Необходимо повысить лимит токенов у API- msgid "API Key not found" msgstr "API-ключ не найден" -#: tools/public_api/models.py:46 +#: tools/public_api/models.py:48 msgid "Expires at" msgstr "Когда заканчивается" -#: tools/public_api/models.py:50 +#: tools/public_api/models.py:52 msgid "API Key" msgstr "API Ключ" -#: tools/public_api/models.py:51 +#: tools/public_api/models.py:53 msgid "API Keys" msgstr "API Ключи" -#: tools/public_api/views/base.py:54 -msgid "Model is blocked by outdating or temporary block, please retry later" -msgstr "Модель заблокирована, т.к перестала обновляться или временно, попробуйте позже" - -#~ msgid "Authorization token" -#~ msgstr "Авторизационный токен" - -#, fuzzy -#~| msgid "Default value" -#~ msgid "Default" -#~ msgstr "Стандартное значение" - #~ msgid "Can search by: model title" #~ msgstr "Можно искать по: Названию модели" @@ -1126,6 +1099,3 @@ msgstr "Модель заблокирована, т.к перестала обн #~ msgid "Generative models" #~ msgstr "Генеративные модели" - -msgid "Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)" -msgstr "Невозможно распознать изображение. (Поддерживаемые форматы: PNG, JPG, JPEG)" @@ -10,7 +10,7 @@ from django_minio_backend import MinioBackend def message_file_upload(instance: 'Message', filename: str): - return f'{instance.content_object.model.slug}_{instance.content_object.user.pk}_{time.time():.0f}.{filename.split(".")[-1]}' + return f"{instance.content_object.model.slug}_{instance.content_object.user.pk}_{time.time():.0f}.{filename.split('.')[-1]}" class Message(models.Model): @@ -36,7 +36,9 @@ class Message(models.Model): null=True, blank=True, ) - created_at = models.DateTimeField(default=timezone.now, verbose_name='Когда создано', editable=False) + created_at = models.DateTimeField( + default=timezone.now, verbose_name='Когда создано', editable=False + ) elapsed_time = models.DurationField( default=timedelta(seconds=0), verbose_name='Затраченное время', @@ -53,7 +55,9 @@ class Message(models.Model): verbose_name='Избранное', ) is_shared = models.BooleanField(default=False, verbose_name='Сообщение в фиде') - content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.DO_NOTHING) + content_type = models.ForeignKey( + ContentType, blank=True, null=True, on_delete=models.DO_NOTHING + ) object_id = models.UUIDField(blank=True, null=True) content_object = fields.GenericForeignKey('content_type', 'object_id') info = models.JSONField( @@ -1,10 +1,10 @@ from uuid import uuid4 -from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.db.models import QuerySet +from backend.settings import AUTH_USER_MODEL from ml_model.models import NeuronModel from .message import Message @@ -48,7 +48,7 @@ class MultipleStore(BaseStore): """ user = models.ForeignKey( - get_user_model(), + AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, verbose_name='Пользователь', @@ -74,7 +74,7 @@ class SingleStore(BaseStore): """ user = models.OneToOneField( - get_user_model(), + AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name='Пользователь', null=True, @@ -53,11 +53,15 @@ class MessagesAPIView(APIView): case 'str': missing_info.update({p.key: p.default if p.default else ''}) case 'float': - missing_info.update({p.key: float(p.default) if p.default else 1.0}) + missing_info.update( + {p.key: float(p.default) if p.default else 1.0} + ) case 'oneof': missing_info.update({p.key: p.default.split(',')[0]}) case 'list': - missing_info.update({p.key: p.default.split(',') if p.default else []}) + missing_info.update( + {p.key: p.default.split(',') if p.default else []} + ) merged_info = info | missing_info i = Message.objects.create( **serializer.validated_data, @@ -91,7 +95,9 @@ class MessageAPIView(APIView): def put(self, request, chat_uid, message_uid, *args, **kwargs): """Put message to favourites""" chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get(uid=message_uid, chats_chats_messages=chat, is_deleted=False) + message = Message.objects.get( + uid=message_uid, chats_chats_messages=chat, is_deleted=False + ) message.is_favourite = True message.save() return Response(status=204) @@ -102,7 +108,9 @@ class MessageAPIView(APIView): def delete(self, request, chat_uid, message_uid, *args, **kwargs): """Delete (Hide to deleted) message""" chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get(pk=message_uid, chats_chats_messages=chat, is_deleted=False) + message = Message.objects.get( + pk=message_uid, chats_chats_messages=chat, is_deleted=False + ) message.is_deleted = True message.save() return Response(status=204) @@ -11,10 +11,7 @@ class MessageSerializer(serializers.ModelSerializer): elapsed_time = serializers.DurationField(read_only=True) from_model = serializers.BooleanField(read_only=True) model = serializers.SlugField( - source='content_object.model', - default=None, - required=False, - read_only=True, + source='content_object.model', default=None, required=False, read_only=True ) is_favourite = serializers.BooleanField(read_only=True) is_sent = serializers.BooleanField(read_only=True) @@ -39,6 +36,7 @@ class MessageSerializer(serializers.ModelSerializer): max_mb_size = 8 if file and file.size > (max_mb_size << 10 << 10): raise ValidationError( - _('The file size cannot exceed %(max_mb_size)d MB') % {'max_mb_size': max_mb_size} + _('The file size cannot exceed %(max_mb_size)d MB') + % {'max_mb_size': max_mb_size} ) return file @@ -31,13 +31,16 @@ class Command(BaseCommand): if isinstance(klass.category, ModelCategory): defaults['category'], _ = ModelCategory.objects.get_or_create( - slug=klass.category.slug, - defaults={'title': klass.category.title}, + slug=klass.category.slug, defaults={'title': klass.category.title} ) - model, _ = NeuronModel.objects.get_or_create(slug=klass.__name__.lower(), defaults=defaults) + model, _ = NeuronModel.objects.get_or_create( + slug=klass.__name__.lower(), defaults=defaults + ) - ModelSettings.objects.get_or_create(model=model, defaults={'is_active': False}) + ModelSettings.objects.get_or_create( + model=model, defaults={'is_active': False} + ) for version in klass.versions: try: @@ -55,7 +58,9 @@ class Command(BaseCommand): for input in klass.inputs: try: - sinput, icreated = ModelInput.objects.get_or_create(model=model, type=input.type) + sinput, icreated = ModelInput.objects.get_or_create( + model=model, type=input.type + ) if not icreated: sinput.required = input.required sinput.save() @@ -1,42 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-29 13:55 - -import core.fields -import django.contrib.postgres.fields -import django.core.validators -import django_minio_backend.models -import ml_model.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0044_alter_modelstat_options'), - ] - - operations = [ - migrations.CreateModel( - name='ModelTag', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='Title')), - ('slug', models.SlugField(max_length=120, unique=True, verbose_name='Slug')), - ('color', core.fields.ColorField(verbose_name='Color')), - ('icon', models.FileField(blank=True, null=True, storage=django_minio_backend.models.MinioBackend('air-models'), upload_to=ml_model.models.model_tag_icon_uploader, validators=[django.core.validators.FileExtensionValidator(['svg'], 'Not SVG-pictures not allowed')], verbose_name='Icon')), - ], - options={ - 'verbose_name': 'Model Tag', - 'verbose_name_plural': 'Model Tags', - }, - ), - migrations.AddField( - model_name='neuronmodel', - name='alternative_titles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, default=list, size=None, verbose_name='Alternative Titles'), - ), - migrations.AddField( - model_name='neuronmodel', - name='tags', - field=models.ManyToManyField(related_name='model_tags', to='ml_model.modeltag', verbose_name='Tags'), - ), - ] @@ -1,27 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-29 21:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0045_modeltag_neuronmodel_alternative_titles_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='modelsettings', - name='authorization_token', - ), - migrations.AlterField( - model_name='modelsettings', - name='is_active', - field=models.BooleanField(default=False, help_text='Модель активна для всех пользователей', verbose_name='Is active'), - ), - migrations.AlterField( - model_name='neuronmodel', - name='tags', - field=models.ManyToManyField(blank=True, related_name='models_tags', to='ml_model.modeltag', verbose_name='Tags'), - ), - ] @@ -1,17 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-29 21:48 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0046_remove_modelsettings_authorization_token_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='modelversion', - name='default', - ), - ] @@ -1,25 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-29 23:08 - -import core.fields -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0047_remove_modelversion_default'), - ] - - operations = [ - migrations.AlterField( - model_name='modeltag', - name='color', - field=core.fields.ColorField(blank=True, null=True, verbose_name='Color'), - ), - migrations.AlterField( - model_name='neuronmodel', - name='category', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='category_models', to='ml_model.modelcategory', verbose_name='Category'), - ), - ] @@ -1,27 +0,0 @@ -# Generated by Django 5.0.11 on 2025-04-01 17:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0048_alter_modeltag_color_alter_neuronmodel_category'), - ] - - operations = [ - migrations.RemoveField( - model_name='modelconfiguration', - name='version', - ), - migrations.AlterField( - model_name='modelversion', - name='slug', - field=models.CharField(max_length=32, verbose_name='Slug'), - ), - migrations.AlterField( - model_name='neuronmodel', - name='description', - field=models.TextField(blank=True, null=True, verbose_name='Description'), - ), - ] @@ -14,7 +14,9 @@ class NeuronModelSelector: def __init__(self, user: CustomUserModel): self.user = user - def get_models_by_input_content_type(self, serialize: bool = False, hidden: bool = False): + def get_models_by_input_content_type( + self, serialize: bool = False, hidden: bool = False + ): models = NeuronModel.objects.prefetch_related( Prefetch( 'model_modelparameters', @@ -25,7 +27,9 @@ class NeuronModelSelector: return NeuronModelSerializer(models, many=True) return models - def get_models_by_output_content_type(self, serialize: bool = False, hidden: bool = False): + def get_models_by_output_content_type( + self, serialize: bool = False, hidden: bool = False + ): models = NeuronModel.objects.prefetch_related( Prefetch( 'model_modelparameters', @@ -7,9 +7,7 @@ from ml_model.services.deepseek import Deepseek from ml_model.services.djourney import Djourney from ml_model.services.epicphotogasm import Epicphotogasm from ml_model.services.flux import Flux -from ml_model.services.fluxproultra import Fluxproultra from ml_model.services.granite import Granite -from ml_model.services.grok import Grok from ml_model.services.iconic import Iconic from ml_model.services.kandinsky import Kandinsky from ml_model.services.lightning import Lightning @@ -18,9 +16,7 @@ from ml_model.services.logoai import Logoai from ml_model.services.midjourney import Midjourney from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen -from ml_model.services.perplexity import Perplexity from ml_model.services.pulid import Pulid -from ml_model.services.qwen import Qwen from ml_model.services.recraft import Recraft from ml_model.services.sdxlemoji import Sdxlemoji from ml_model.services.stablediffusion import Stablediffusion @@ -3,8 +3,6 @@ import itertools import logging import subprocess import time - -from django.utils.translation import gettext_lazy as _ from datetime import timedelta from decimal import Decimal from io import BufferedReader, BytesIO @@ -22,26 +20,23 @@ from langchain.agents import AgentExecutor, create_structured_chat_agent from langchain.chains import ConversationChain from langchain_community.tools.google_serper import GoogleSerperResults from langchain_core.chat_history import InMemoryChatMessageHistory -from langchain_core.messages import ( - AIMessage, - BaseMessage, - HumanMessage, - SystemMessage, -) +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage from langchain_core.prompts.prompt import PromptTemplate from langchain_core.runnables import RunnableWithMessageHistory from langchain_openai.chat_models import ChatOpenAI from langchain_text_splitters import RecursiveCharacterTextSplitter -from PIL import Image, UnidentifiedImageError +from PIL import Image from PyPDF2 import PdfReader -from backend import settings from messages.models import BaseStore, Message from ml_model.constants import TEMPORARY_TEST_TEXT from ml_model.exceptions import GenerationException from ml_model.models import ( + ModelCategory, ModelConfiguration, - NeuronModel, + ModelInput, + ModelParameter, + ModelVersion, ) from ml_model.services.base import SimpleService from payments.exceptions.insufficient_balance import InsufficientBalance @@ -52,47 +47,72 @@ from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore +from backend import settings + class Chatgpt(SimpleService): """ ChatGPT Service contains abstract method make, which makes a generation """ + title = 'ChatGPT' + description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' + + versions = [ + ModelVersion(name='GPT-4o1', default=True, slug='o1-preview'), + ModelVersion(name='GPT-4o1 Mini', slug='o1-mini'), + ModelVersion(name='GPT-4omni Mini', slug='gpt-4o-mini'), + ModelVersion(name='GPT-4omni', slug='gpt-4o'), + ] + + inputs = [ + ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE), + ModelInput(type=ModelInput.TypeChoices.PDF), + ModelInput(type=ModelInput.TypeChoices.DOCX), + ModelInput(type=ModelInput.TypeChoices.DOC), + ] + + parameters = [ + ModelParameter( + name='Температура', + key='temperature', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 0.1, 'end': 2.0, 'step': 0.1, 'default': 0.5}, + ), + ModelParameter( + name='Лучший процент', + key='top_p', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 0.1, 'end': 2.0, 'step': 0.1, 'default': 0.5}, + ), + ModelParameter( + name='Штраф за присутствие', + key='presence', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 0.1, 'end': 2.0, 'step': 0.1, 'default': 0.5}, + ), + ModelParameter( + name='Использовать интернет', + key='use_web', + type=ModelParameter.TypeChoices.BOOL, + values={'default': True}, + ), + ] + category = ModelCategory(title='Чат-боты', slug='chat-bots') + TOKENS_COST = { - 'o3-mini': { - 'input': Decimal('0.000605'), - 'output': Decimal('0.002420'), - }, - 'o1-preview': { - 'input': Decimal('0.008250'), - 'output': Decimal('0.033000'), - }, - 'o1-mini': { - 'input': Decimal('0.001650'), - 'output': Decimal('0.006600'), - }, - 'gpt-4o-mini': { - 'input': Decimal('0.000083'), - 'output': Decimal('0.000330'), - }, - 'gpt-4o': { - 'input': Decimal('0.001375'), - 'output': Decimal('0.005500'), - }, - 'gpt-4.5-preview': { - 'input': Decimal('0.075'), - 'output': Decimal('0.075'), - }, + 'o3-mini': {'input': Decimal('0.000605'), 'output': Decimal('0.002420')}, + 'o1-preview': {'input': Decimal('0.008250'), 'output': Decimal('0.033000')}, + 'o1-mini': {'input': Decimal('0.001650'), 'output': Decimal('0.006600')}, + 'gpt-4o-mini': {'input': Decimal('0.000083'), 'output': Decimal('0.000330')}, + 'gpt-4o': {'input': Decimal('0.001375'), 'output': Decimal('0.005500')}, } def __init__(self, store: BaseStore) -> None: super().__init__(store) self.logger = logging.getLogger(self.__class__.__name__) - @property - def neuron_model(self): - return NeuronModel.objects.get(title='ChatGPT') - def make( self, input_message: Message, @@ -111,24 +131,24 @@ class Chatgpt(SimpleService): if file_extension == '.pdf': chunks = self.split_text_to_chunks(self.get_pdf_data(file)) elif file_extension in ('.doc', '.docx'): - chunks = self.split_text_to_chunks(self.get_word_data(file_extension, file)) + chunks = self.split_text_to_chunks( + self.get_word_data(file_extension, file) + ) else: image = file if image: - try: - kind = filetype.guess(input_message.file.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - normalized_image = Image.open(image) - format = 'jpeg' if kind.extension == 'jpg' else kind.extension - buf = BytesIO() - normalized_image.save(buf, format=format) - image_url = f'data:{mime},base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' - buf.close() - image_size = normalized_image.size - image_data = {'type': 'image_url', 'image_url': {'url': image_url}} - input_content.append(image_data) - except UnidentifiedImageError: - raise Exception(_('Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)')) + kind = filetype.guess(input_message.file.read(20)) + mime = kind.mime if kind else 'application/octet-stream' + normalized_image = Image.open(image) + format = 'jpeg' if kind.extension == 'jpg' else kind.extension + buf = BytesIO() + normalized_image.save(buf, format=format) + image_url = ( + f'data:{mime},base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + ) + buf.close() + image_size = normalized_image.size + input_content.append({'type': 'image_url', 'image_url': {'url': image_url}}) for proxy in Proxy.objects.all(): self.llm = ChatOpenAI( model=model_name, @@ -158,7 +178,6 @@ class Chatgpt(SimpleService): 'o1-preview', 'o1-mini', 'o3-mini', - 'gpt-4.5-preview', ): self.llm.tiktoken_model_name = 'gpt-4' chat_history = self.get_chat_history() @@ -168,53 +187,21 @@ class Chatgpt(SimpleService): ) llm_input = HumanMessage(content=input_content) if file and not image: - input_tokens = self.count_text_tokens([*chat_history.messages, llm_input, *chunks]) + input_tokens = self.count_text_tokens( + [*chat_history.messages, llm_input, *chunks] + ) elif image: input_tokens = self.count_text_tokens([llm_input]) else: - input_tokens = self.count_text_tokens([*chat_history.messages, llm_input]) - output_tokens = 0 - self.assert_enough_balance(input_tokens, image_size, model=self.llm.model_name) - if image and model_name not in ('o3-mini', 'gpt-4.5-preview'): + input_tokens = self.count_text_tokens( + [*chat_history.messages, llm_input] + ) + self.assert_enough_balance( + input_tokens, image_size, model=self.llm.model_name + ) + if image: response = self.llm.invoke([llm_input]) chat_history.add_ai_message(response) - elif model_name in ('o3-mini', 'gpt-4.5-preview'): - with httpx.Client( - base_url='https://api.openai.com/v1', - proxy=f'{proxy.protocol}://{proxy.address}', - headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, - timeout=600, - ) as client: - messages = [ - {'role': 'user' if isinstance(msg, HumanMessage) else 'assistant', 'content': msg.content} - for msg in chat_history.messages - ] - if image: - messages[-1]['content'] = [ - {'type': 'text', 'text': input_message.content}, - image_data, - ] - resp = client.post( - 'chat/completions', - json={ - 'model': model_name, - 'messages': messages - }, - ) - if ( - (data := resp.json()) - and data.get('choices') - and ( - content := ','.join( - [choice['message']['content'] for choice in data.get('choices')] - ) - ) - ): - input_tokens = resp.json()['usage']['prompt_tokens'] - output_tokens = resp.json()['usage']['completion_tokens'] - response = AIMessage(content=content) - else: - raise Exception('GPT not answer correctly, please retry later') elif file: human_messages = [] chunk_responses = ['Содержание файла: '] @@ -234,7 +221,11 @@ class Chatgpt(SimpleService): ) chunk_responses.append(response.content) combined_summary = ' '.join(chunk_responses) - user_prompt = input_message.content if input_message.content.split() else 'Суммируй текст' + user_prompt = ( + input_message.content + if input_message.content.split() + else 'Суммируй текст' + ) question_content = ( f'Вот краткое содержание каждого чанка:\n' f'{combined_summary}\nОтветьте на вопрос по содержанию файла: {user_prompt}' @@ -262,7 +253,9 @@ class Chatgpt(SimpleService): 'input': [llm_input], 'chat_history': chat_history.messages + [ - SystemMessage(content='Учитывай язык диалога перед выдачей ответа'), + SystemMessage( + content='Учитывай язык диалога перед выдачей ответа' + ), SystemMessage( content='Никому не говори, что ты бот и не можешь найти информацию в интернете' ), @@ -270,6 +263,37 @@ class Chatgpt(SimpleService): } )['output'] ) + elif model_name == 'o3-mini': + with httpx.Client( + base_url='https://api.openai.com/v1', + proxy=f'{proxy.protocol}://{proxy.address}', + headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, + timeout=600, + ) as client: + resp = client.post( + 'chat/completions', + json={ + 'model': model_name, + 'messages': [ + {'role': 'user', 'content': input_message.content}, + ], + }, + ) + if ( + (data := resp.json()) + and data.get('choices') + and ( + content := ','.join( + [ + choice['message']['content'] + for choice in data.get('choices') + ] + ) + ) + ): + response = AIMessage(content=content) + else: + raise Exception('GPT not answer correctly, please retry later') else: # Somehow this chain doesn't support Vision, even though ChatOpenAI (above) does. response = conversation.invoke( @@ -278,20 +302,27 @@ class Chatgpt(SimpleService): ) chat_history.add_ai_message(response) - if output_tokens == 0: - output_tokens = self.count_text_tokens([response]) + output_tokens = self.count_text_tokens([response]) if file and not image: output_tokens += self.count_text_tokens( [AIMessage(chunk_response) for chunk_response in chunk_responses] ) - if image and normalized_image and model_name not in ('o3-mini', 'gpt-4.5-preview'): - self.logger.info(f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}') + if image and normalized_image: + self.logger.info( + f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}' + ) input_tokens += self.count_image_tokens(normalized_image.size, model_name) - self.logger.info(f'Input количество токенов для {model_name} - {input_tokens}') - self.logger.info(f'Output количество токенов для {model_name} - {output_tokens}') - self.logger.info(f'Общее количество токенов для {model_name} - {input_tokens + output_tokens}') + self.logger.info( + f'Input количество токенов для {model_name} - {input_tokens}' + ) + self.logger.info( + f'Output количество токенов для {model_name} - {output_tokens}' + ) + self.logger.info( + f'Общее количество токенов для {model_name} - {input_tokens + output_tokens}' + ) process_time = timedelta(seconds=time.time() - start_time) self.handle_invoice( @@ -305,17 +336,15 @@ class Chatgpt(SimpleService): raise GenerationException def get_chat_history( - self, - message_limit: int = 10, - token_limit: int = 580, # ~ 1 AIR Token with GPT 3.5 + self, + message_limit: int = 10, + token_limit: int = 580, # ~ 1 AIR Token with GPT 3.5 ) -> InMemoryChatMessageHistory: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( - chats_chats_messages=self.store, - is_deleted=False, - is_sent=True, + chats_chats_messages=self.store, is_deleted=False, is_sent=True ).order_by('-created_at')[:message_limit] ) ) @@ -349,10 +378,7 @@ class Chatgpt(SimpleService): return memory def assert_enough_balance( - self, - input_tokens: int, - image_size: tuple, - model: str = 'gpt-3.5-turbo', + self, input_tokens: int, image_size: tuple, model: str = 'gpt-3.5-turbo' ): balance = PaymentPlanSelector(self.store.user).get_current_balance() total_tokens = input_tokens @@ -365,12 +391,7 @@ class Chatgpt(SimpleService): raise InsufficientBalance(balance, input_cost) def calculate_price( - self, - input_tokens: int, - output_tokens: int, - model: str, - *args, - **kwargs, + self, input_tokens: int, output_tokens: int, model: str, *args, **kwargs ) -> Decimal: price = ( input_tokens * self.TOKENS_COST[model]['input'] @@ -393,7 +414,11 @@ class Chatgpt(SimpleService): if max(width, height) > 2048: a_ratio = width / height - width, height = (2048, int(2048 / a_ratio)) if a_ratio > 1 else (int(2048 * a_ratio), 2048) + width, height = ( + (2048, int(2048 / a_ratio)) + if a_ratio > 1 + else (int(2048 * a_ratio), 2048) + ) if width >= height and height > 768: width, height = int((768 / height) * width), 768 elif height > width and width > 768: @@ -412,8 +437,8 @@ class Chatgpt(SimpleService): if isinstance(message.content, str): total_tokens += len(encoding.encode(message.content)) elif any(isinstance(item, dict) for item in message.content): - total_tokens += len( - encoding.encode(''.join([input_data.get('text', '') for input_data in message.content])) + total_tokens += len(encoding.encode( + ''.join([input_data.get('text', '') for input_data in message.content])) ) else: total_tokens += len(encoding.encode(''.join(message.content))) @@ -439,7 +464,9 @@ class Chatgpt(SimpleService): if raw_text.strip(): return f'Содержимое файла: f{raw_text}' else: - return 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' + return ( + 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' + ) def get_word_data(self, extension: str, word_file: UploadedFile) -> str: """ @@ -468,7 +495,9 @@ class Chatgpt(SimpleService): if text.strip(): return f'Это текст, извлечённый из загруженного WORD-файла:\n{text}' else: - return 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' + return ( + 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' + ) def split_text_to_chunks( self, raw_text: str, chunk_size: int = 100_000, overlap: int = 300 @@ -487,10 +516,7 @@ class Chatgpt(SimpleService): return [HumanMessage(chunk) for chunk in chunks] def save_results( - self, - results: list[BaseMessage], - elapsed_time: timedelta, - save: bool = True, + self, results: list[BaseMessage], elapsed_time: timedelta, save: bool = True ) -> list[Message]: messages = [ Message( @@ -537,7 +563,9 @@ class Chatgpt(SimpleService): self.llm.get_num_tokens_from_messages(chat_history.messages), self.llm.model_name, ) - msgs = self.save_results([chat_history.messages[-1]], process_time, save) + msgs = self.save_results( + [chat_history.messages[-1]], process_time, save + ) return msgs @classmethod @@ -1,20 +1,11 @@ -import base64 import time +from _decimal import Decimal from datetime import timedelta -from decimal import Decimal -from io import BytesIO -from typing import Any, Iterator - -import filetype -from django.db.models.fields.files import FieldFile -from PIL import Image from messages.models import Message +from ml_model.models import ModelCategory from ml_model.services.base import SimpleService -from ml_model.tasks import openrouter_run -from tools.chats.models import Chat -from tools.copywrite.models import Copywrite -from tools.public_api.models import APIStore +from ml_model.tasks import claude_run class Claude(SimpleService): @@ -23,29 +14,36 @@ class Claude(SimpleService): contains abstract method make, which makes a generation """ - TOKENS_COST = { - 'claude-3.7-sonnet:thinking': { - 'input': Decimal('3000'), - 'output': Decimal('3000'), - 'input_imgs': Decimal('960'), - }, # 1M tokens + muted = True + + title = 'Claude' + description = 'Нейросеть, способная генерировать качественный текст из вашего промпта' + category = ModelCategory(title='Чат-боты', slug='chat-bots') + versions = [] + inputs = [] + parameters = [] + + muted = True + + TOKEN_PAYMENT_RULES = { + 'claude-instant-1.2': Decimal(0.6), + 'claude-2.1': Decimal(2.572), } - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int, image: FieldFile - ) -> Decimal: - price_map = self.TOKENS_COST[version.split('/')[1]] + def __init__(self, store): + super().__init__(store) + + def calculate_price(self, model_name: str, usage: dict[str, int]) -> Decimal: price = ( - input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 + usage['input_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] + + usage['output_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] ) - if image: - price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + def save_results(self, r: str, t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( - content=content, + content=r, content_object=self.store, elapsed_time=t, ) @@ -55,67 +53,17 @@ class Claude(SimpleService): return msgs def make(self, input_message: Message, save: bool = True) -> list[Message]: + callback_data = dict( + { + 'messages': [{'role': 'user', 'content': input_message.content}], + **input_message.info, + } + ) start_time = time.time() - version = f'anthropic/{input_message.info.pop("version", "claude-3.7-sonnet:thinking")}' - callback_data = {'provider': {'order': ['Anthropic']}, **input_message.info} - messages = self.get_chat_history() - image = input_message.file - if image: - kind = filetype.guess(image.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - normalized_image = Image.open(image) - format = 'jpeg' if kind.extension == 'jpg' else kind.extension - buf = BytesIO() - normalized_image.save(buf, format=format) - image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' - buf.close() - messages[-1]['content'] = [ - {'type': 'text', 'text': input_message.content}, - {'type': 'image_url', 'image_url': {'url': image_url}}, - ] - result = openrouter_run(version, messages, callback_data, 'Claude') + result = claude_run(callback_data) process_time = timedelta(seconds=(time.time() - start_time)) + msgs = self.save_results(result['content']['text'], process_time, save) self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - output_tokens=result[2], - image=image, + self.neuron_model, model_name=input_message['model'], usage=result['usage'] ) - msgs = self.save_results(result[0], process_time) return msgs - - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({'role': 'assistant', 'content': content}) - else: - memory.append({'role': 'user', 'content': content}) - character_length = sum(len(content['content']) for content in memory) - while character_length > max_character_limit: - memory.pop(0) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -4,6 +4,7 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message +from ml_model.models import ModelCategory from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -14,13 +15,28 @@ class Codellama(SimpleService): contains abstract method make, which makes a generation """ + muted = True + + title = 'Code LLaMA-34B' + description = 'Нейросеть, способная генерировать код из вашего контекста' + price = Decimal('0.558') + category = ModelCategory(title='Код', slug='code') + versions = [] + inputs = [] + parameters = [] + _CALLBACK = 'meta/codellama-34b:ffccbaa0d78e4dea7a9d46f29debaf390c2087c357e0632381f127382d3bf2fd' def calculate_price(self, messages: list[Message]) -> Decimal: - price = sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) * self.price + price = ( + sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) + * self.price + ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, r: Iterator[Any], t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=''.join(word for word in r), @@ -7,6 +7,7 @@ import requests from django.core.files import File from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -17,7 +18,70 @@ class Dalle(SimpleService): contains abstract method make, which makes a generation """ - PRICE = Decimal('2') + title = 'Dalle' + description = 'Нейросеть, способная генерировать фотографии из вашего текста' + category = ModelCategory(title='Изображения', slug='images') + versions = [ + ModelVersion(name='Dalle 3', slug='sdxl-lightning-4step', default=True), + ] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] + parameters = [ + ModelParameter( + name='Негативный промпт', + key='negative_prompt', + type=ModelParameter.TypeChoices.STR, + ), + ModelParameter( + name='Ширина', + key='width', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1024, 'end': 1280, 'step': 256, 'default': 1024}, + ), + ModelParameter( + name='Высота', + key='height', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1024, 'end': 1280, 'step': 256, 'default': 1024}, + ), + ModelParameter( + name='Планировщик', + key='scheduler', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': [ + "DDIM", + "DPMSolverMultistep", + "HeunDiscrete", + "KarrasDPM", + "K_EULER_ANCESTRAL", + "K_EULER", + "PNDM", + "DPM++2MSDE" + ], + 'default': 'K_EULER', + }, + ), + ModelParameter( + name='Количество изображений', + key='num_outputs', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='Точность запроса', + key='guidance_scale', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 0, 'end': 50, 'step': 1, 'default': 0}, + ), + ModelParameter( + name='Шаги предобработки', + key='num_inference_steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 10, 'step': 1, 'default': 4}, + ), + ] + + PRICE = Decimal('1') _CALLBACK = ( 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' @@ -27,7 +91,9 @@ class Dalle(SimpleService): price = input_message.info.get('num_outputs', 1) * self.PRICE return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -77,7 +77,9 @@ class Deepl(SimpleService): ) ) else: - messages.append(Message(content=content, content_object=self.store, elapsed_time=t)) + messages.append( + Message(content=content, content_object=self.store, elapsed_time=t) + ) if save: return Message.objects.bulk_create(messages) return messages @@ -99,8 +101,7 @@ class Deepl(SimpleService): is_file = False start_time = time.time() languages = self.convert_languages( - input_message.info.get('source_lang'), - input_message.info.get('target_lang'), + input_message.info.get('source_lang'), input_message.info.get('target_lang') ) callback_data = dict( { @@ -134,10 +135,7 @@ class Deepl(SimpleService): translation = result.get() process_time = timedelta(seconds=(time.time() - start_time)) msgs = self.save_results( - content=str(translation), - is_file=is_file, - t=process_time, - save=save, + content=str(translation), is_file=is_file, t=process_time, save=save ) self.handle_invoice(self.neuron_model, messages=msgs) return msgs @@ -16,10 +16,7 @@ class Deepseek(SimpleService): 'input': Decimal('107.800') / 1_000_000, 'output': Decimal('195.800') / 1_000_000, }, - 'deepseek/deepseek-r1:free': { - 'input': Decimal('0'), - 'output': Decimal('0'), - }, + 'deepseek/deepseek-r1:free': {'input': Decimal('0'), 'output': Decimal('0')}, 'deepseek/deepseek-r1': { 'input': Decimal('176.0') / 1_000_000, 'output': Decimal('528.0') / 1_000_000, @@ -27,12 +24,20 @@ class Deepseek(SimpleService): } PRICE_BIAS = Decimal('0.05') - def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: + def calculate_price( + self, version: str, input_tokens: int, output_tokens: int + ) -> Decimal: price_map = self.TOKENS_COST[version] - price = input_tokens * price_map['input'] + output_tokens * price_map['output'] + self.PRICE_BIAS + price = ( + input_tokens * price_map['input'] + + output_tokens * price_map['output'] + + self.PRICE_BIAS + ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, content: Iterator[Any], t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=content, @@ -62,7 +67,11 @@ class Deepseek(SimpleService): if ( (data := resp.json()) and data.get('choices') - and (content := ','.join([choice['message']['content'] for choice in data.get('choices')])) + and ( + content := ','.join( + [choice['message']['content'] for choice in data.get('choices')] + ) + ) ): result = content process_time = timedelta(seconds=(time.time() - start_time)) @@ -25,7 +25,7 @@ class Djourney(SimpleService): versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), + ModelInput(type=ModelInput.TypeChoices.IMAGE) ] parameters = [ ModelParameter( @@ -63,7 +63,7 @@ class Djourney(SimpleService): 'KarrasDPM', 'K_EULER_ANCESTRAL', 'K_EULER', - 'PNDM', + 'PNDM' ], 'default': 'K_EULER', }, @@ -84,13 +84,17 @@ class Djourney(SimpleService): PRICE = Decimal('0.558') - _CALLBACK = 'lorenzomarines/d-journey:2d84f3049a0b3ed1a20fc657f39c4b1bdef1f7a8ea0ea8b6258e7c37296e039a' + _CALLBACK = ( + 'lorenzomarines/d-journey' + ':2d84f3049a0b3ed1a20fc657f39c4b1bdef1f7a8ea0ea8b6258e7c37296e039a' + ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -98,7 +102,7 @@ class Djourney(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png'), + file=File(BytesIO(requests.get(image).content), '.png') ) ) if save: @@ -75,7 +75,8 @@ class Epicphotogasm(SimpleService): translated_prompt = self.translate_prompt(input_message.content) activation_prompt = f'{translated_prompt}, cinematic' negative_prompt = self.translate_prompt( - input_message.info.pop('negative_prompt', '') + ', UnrealisticDream, BadDream, EasyNegative' + input_message.info.pop('negative_prompt', '') + + ', UnrealisticDream, BadDream, EasyNegative' ) callback_data = dict( prompt=activation_prompt, @@ -1,19 +1,21 @@ +import base64 import time from datetime import timedelta from decimal import Decimal from io import BytesIO +import filetype import requests from django.core.files import File -from messages.models import Message +from backend import settings +from messages.models import BaseStore, Message from ml_model.models import ( ModelCategory, ModelInput, ModelParameter, ModelPaymentRule, ModelVersion, - NeuronModel, ) from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -29,18 +31,40 @@ class Flux(SimpleService): description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [ - ModelVersion(name='Flux-Schnell', slug='flux-schnell'), + ModelVersion(name='Flux-Schnell', slug='flux-schnell', default=True), + ModelVersion(name='Flux-Pro1.1', slug='flux-pro-1.1'), + ModelVersion(name='Flux-Dev', slug='flux-dev'), + ModelVersion(name='Ultra', slug='flux-1.1-pro-ultra'), ] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( - name='Ускорение генерации', + name='Ширина', + key='width', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 256, 'end': 1440, 'step': 32, 'default': 1024}, + ), + ModelParameter( + name='Высота', + key='height', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 256, 'end': 1440, 'step': 32, 'default': 768}, + ), + ModelParameter( + name='Ускорить', key='go_fast', type=ModelParameter.TypeChoices.BOOL, values={'default': True}, ), + ModelParameter( + name='Приближенность к запросу', + key='guidance', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 0, 'end': 10, 'step': 1, 'default': 3}, + ), ModelParameter( name='Мегапиксели', key='megapixels', @@ -60,7 +84,13 @@ class Flux(SimpleService): values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, ), ModelParameter( - name='Количество шагов обработки', + name='Количество шагов вывода', + key='num_inference_steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 50, 'step': 1, 'default': 28}, + ), + ModelParameter( + name='Количество шагов вывода', key='num_inference_steps', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 1, 'end': 4, 'step': 1, 'default': 4}, @@ -87,41 +117,91 @@ class Flux(SimpleService): }, ), ModelParameter( - name='Качество вывода (в %)', + name='Качество вывода', key='output_quality', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 0, 'end': 100, 'step': 1, 'default': 80}, ), + ModelParameter( + name='Апсемплинг', + key='prompt_upsampling', + type=ModelParameter.TypeChoices.BOOL, + values={'default': False}, + ), + ModelParameter( + name='Отключить обработку', + key='raw', + type=ModelParameter.TypeChoices.BOOL, + values={'default': False}, + ), ] payments_rules = { versions[0].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, cost=0.3, - coefficient=10.00, + coefficient=5.00, + ), + versions[1].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=4.00, + coefficient=2.00, + ), + versions[2].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=2.5, + coefficient=2.00, + ), + versions[3].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=6.00, + coefficient=2.00, ), } + _CALLBACK_BASE = 'black-forest-labs/' - @property - def neuron_model(self): - return NeuronModel.objects.get(title='Flux') + def _call_bfl_api(self, payload: dict) -> list: + bfl_headers = { + 'Content-Type': 'application/json', + 'X-Key': settings.FLUX_API_KEY, + } + bfl_urls = { + 'generate': 'https://api.bfl.ml/v1/', + 'get': 'https://api.bfl.ml/v1/get_result?id=', + } + response = requests.post( + url=f'{bfl_urls['generate']}{payload['version']}', + headers=bfl_headers, + json=payload, + ) + if response.status_code != 200: + raise Exception(response.json()) + result = requests.get(url=f'{bfl_urls['get']}{response.json().get('id')}', headers=bfl_headers) + while result.json()['status'] not in ('Ready', 'Error'): + result = requests.get(url=f'{bfl_urls['get']}{response.json().get('id')}', headers=bfl_headers) + return result.json()['result']['sample'] def calculate_price(self, input_message: Message) -> Decimal: - version_slug = input_message.info.get('version', 'flux-schnell') + version_slug = input_message.info.get('version', 'flux-pro-1.1') payment_rule = self.payments_rules[version_slug] if not payment_rule.pk: payment_rule.model = self.neuron_model payment_rule.save() - return payment_rule.rate * input_message.info.get('num_outputs', 1) - + if version_slug in (self.versions[1].slug,): + return payment_rule.rate * input_message.info.get('num_outputs', 1) + else: + return payment_rule.rate def save_results( - self, - prompt: str, - images: list, - time: timedelta, - save: bool = True, + self, + prompt: str, + images: list, + time: timedelta, + save: bool = True, ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -145,11 +225,21 @@ class Flux(SimpleService): **input_message.info, } ) - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data.get('version', 'flux-schnell')}', - callback_data, - ) - images = runner if isinstance(runner, list) else [runner] + if input_message.file: + kind = filetype.guess(input_message.file.read(20)) + mime = kind.mime if kind else 'application/octet-stream' + input_message.file.seek(0) + image = ( + f"data:{mime};base64," + f"{base64.b64encode(input_message.file.read()).decode('utf-8')}" + ) + input_message.file.close() + callback_data.update({'image': image}) + if callback_data['version'] == 'flux-pro-1.1': + images = [self._call_bfl_api(payload=callback_data)] + else: + runner = replicate_run(f'{self._CALLBACK_BASE}{callback_data['version']}', callback_data) + images = runner if isinstance(runner, list) else [runner] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message) msgs = self.save_results(input_message.content, images, process_time, save) @@ -1,244 +0,0 @@ -import base64 -import time -from datetime import timedelta -from decimal import Decimal -from io import BytesIO - -import filetype -import requests -from django.core.files import File - -from backend import settings -from messages.models import Message -from ml_model.models import ( - ModelCategory, - ModelInput, - ModelParameter, - ModelPaymentRule, - ModelVersion, - NeuronModel, -) -from ml_model.services.base import SimpleService -from ml_model.tasks import replicate_run - - -class Fluxproultra(SimpleService): - """ - Flux Service - contains abstract method make, which makes a generation - """ - - title = 'Flux Pro Ultra' - description = 'Нейросеть, способная генерировать картинки из вашего текста' - category = ModelCategory(title='Изображения', slug='images') - versions = [ - ModelVersion(name='Flux-Pro1.1', slug='flux-pro-1.1'), - ModelVersion(name='Flux-Dev', slug='flux-dev'), - ModelVersion(name='Ultra', slug='flux-1.1-pro-ultra'), - ] - inputs = [ - ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), - ] - parameters = [ - ModelParameter( - name='Ширина', - key='width', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 256, 'end': 1440, 'step': 32, 'default': 1024}, - ), - ModelParameter( - name='Высота', - key='height', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 256, 'end': 1440, 'step': 32, 'default': 768}, - ), - ModelParameter( - name='Ускорение генерации', - key='go_fast', - type=ModelParameter.TypeChoices.BOOL, - values={'default': True}, - ), - ModelParameter( - name='Приближенность к запросу', - key='guidance', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 0, 'end': 10, 'step': 1, 'default': 3}, - ), - ModelParameter( - name='Мегапиксели', - key='megapixels', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - '1', - '0.25', - ], - 'default': '1', - }, - ), - ModelParameter( - name='Количество изображений', - key='num_outputs', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='Количество шагов вывода', - key='num_inference_steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 50, 'step': 1, 'default': 28}, - ), - ModelParameter( - name='Соотношение сторон', - key='aspect_ratio', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - '1:1', - '16:9', - '21:9', - '3:2', - '2:3', - '4:5', - '5:4', - '3:4', - '4:3', - '9:16', - '9:21', - ], - 'default': '1:1', - }, - ), - ModelParameter( - name='Качество вывода (в %)', - key='output_quality', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 0, 'end': 100, 'step': 1, 'default': 80}, - ), - ModelParameter( - name='Апсемплинг', - key='prompt_upsampling', - type=ModelParameter.TypeChoices.BOOL, - values={'default': False}, - ), - ModelParameter( - name='Отключить пост-обработку', - key='raw', - type=ModelParameter.TypeChoices.BOOL, - values={'default': False}, - ), - ] - payments_rules = { - versions[0].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=4.00, - coefficient=2.00, - ), - versions[1].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=2.5, - coefficient=2.00, - ), - versions[2].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=6.00, - coefficient=2.00, - ), - } - - _CALLBACK_BASE = 'black-forest-labs/' - - @property - def neuron_model(self): - return NeuronModel.objects.get(title='Flux Pro Ultra') - - def _call_bfl_api(self, payload: dict) -> list: - bfl_headers = { - 'Content-Type': 'application/json', - 'X-Key': settings.FLUX_API_KEY, - } - bfl_urls = { - 'generate': 'https://api.bfl.ml/v1/', - 'get': 'https://api.bfl.ml/v1/get_result?id=', - } - response = requests.post( - url=f'{bfl_urls["generate"]}{payload["version"]}', - headers=bfl_headers, - json=payload, - ) - if response.status_code != 200: - raise Exception(response.json()) - result = requests.get( - url=f'{bfl_urls["get"]}{response.json().get("id")}', - headers=bfl_headers, - ) - while result.json()['status'] not in ('Ready', 'Error'): - result = requests.get( - url=f'{bfl_urls["get"]}{response.json().get("id")}', - headers=bfl_headers, - ) - return result.json()['result']['sample'] - - def calculate_price(self, input_message: Message) -> Decimal: - version_slug = input_message.info.get('version', 'flux-pro-1.1') - payment_rule = self.payments_rules[version_slug] - if not payment_rule.pk: - payment_rule.model = self.neuron_model - payment_rule.save() - if version_slug in (self.versions[1].slug,): - return payment_rule.rate * input_message.info.get('num_outputs', 1) - else: - return payment_rule.rate - - def save_results( - self, - prompt: str, - images: list, - time: timedelta, - save: bool = True, - ) -> list[Message]: - messages: list[Message] = [] - for image in images: - messages.append( - Message( - content_object=self.store, - elapsed_time=time, - content=prompt, - file=File(BytesIO(requests.get(image).content), '.png'), - ) - ) - if save: - return Message.objects.bulk_create(messages) - return messages - - def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - callback_data = dict( - { - 'prompt': self.translate_prompt(input_message.content), - **input_message.info, - } - ) - if input_message.file: - kind = filetype.guess(input_message.file.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - input_message.file.seek(0) - image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' - input_message.file.close() - callback_data.update({'image': image}) - if callback_data['version'] == 'flux-pro-1.1': - images = [self._call_bfl_api(payload=callback_data)] - else: - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data["version"]}', - callback_data, - ) - images = runner if isinstance(runner, list) else [runner] - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, input_message) - msgs = self.save_results(input_message.content, images, process_time, save) - return msgs @@ -50,8 +50,8 @@ class Granite(SimpleService): def __init__(self, store: BaseStore) -> None: super().__init__(store) self.urls = { - 'generate': 'https://api.replicate.com/v1/models/ibm-granite/granite-3.0-8b-instruct/predictions', - 'get': 'https://api.replicate.com/v1/predictions', + 'generate': 'https://api.replicate.com/v1/models/ibm-granite/granite-3.0-8b-instruct/', + 'get': 'https://api.replicate.com/v1/predictions/', } def _call_api(self, payload: dict) -> list: @@ -61,31 +61,29 @@ class Granite(SimpleService): } data = {'input': payload} response = requests.post( - url=self.urls['generate'], + url=f'{self.urls["generate"]}predictions', headers=headers, json=data, ) if response.status_code != 201: raise Exception(response.json()) result = requests.get( - url=f'{self.urls["get"]}{response.json().get("id")}', - headers=headers, + url=f'{self.urls["get"]}{response.json().get("id")}', headers=headers ) - while result.json()['status'] not in ( - 'succeeded', - 'failed', - 'canceled', - ): + while result.json()['status'] not in ('succeeded', 'failed', 'canceled'): result = requests.get( - url=f'{self.urls["get"]}/{response.json().get("id")}', - headers=headers, + url=f'{self.urls["get"]}{response.json().get("id")}', headers=headers ) return result.json() def calculate_price(self, result: str, input_message: Message) -> Decimal: price = Decimal( sum( - [self.TOKEN_PAYMENT_RULES['granite-output'] / 1_000_000 * len(result.split(' '))] + [ + self.TOKEN_PAYMENT_RULES['granite-output'] + / 1_000_000 + * len(result.split(' ')) + ] + [ self.TOKEN_PAYMENT_RULES['granite-input'] / 1_000_000 @@ -95,7 +93,9 @@ class Granite(SimpleService): ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, result: str, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, result: str, time: timedelta, save: bool = True + ) -> list[Message]: msgs: list[Message] = [ Message( content_object=self.store, @@ -1,120 +0,0 @@ -import base64 -import time -from datetime import timedelta -from decimal import Decimal -from io import BytesIO -from typing import Any, Iterator, Dict - -import filetype -from PIL import Image -from django.db.models.fields.files import FieldFile - -from messages.models import Message -from ml_model.services.base import SimpleService -from ml_model.tasks import openrouter_run -from tools.chats.models import Chat -from tools.copywrite.models import Copywrite -from tools.public_api.models import APIStore - - -class Grok(SimpleService): - """ - Grok Service - contains abstract method make, which makes a generation - """ - - TOKENS_COST = { - 'grok-2-vision-1212': { - 'input': Decimal('2000'), - 'output': Decimal('2000'), - 'input_imgs': Decimal('720'), - }, # 1M tokens and 1K imgs - } - - def calculate_price(self, version: str, input_tokens: int, output_tokens: int, image: FieldFile) -> Decimal: - price_map = self.TOKENS_COST[version.split('/')[1]] - price = ( - input_tokens * price_map['input'] / 1_000_000 - + output_tokens * price_map['output'] / 1_000_000 - ) - if image: - price += price_map['input_imgs'] / 1_000 - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - - def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] - if save: - return Message.objects.bulk_create(msgs) - return msgs - - def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - version = f'x-ai/{input_message.info.pop("version", "grok-2-vision-1212")}' - callback_data = {**input_message.info} - messages = self.get_chat_history() - image = input_message.file - if image: - kind = filetype.guess(image.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - normalized_image = Image.open(image) - format = 'jpeg' if kind.extension == 'jpg' else kind.extension - buf = BytesIO() - normalized_image.save(buf, format=format) - image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' - buf.close() - messages[-1]['content'] = [ - {'type': 'text', 'text': input_message.content}, - {'type': 'image_url', 'image_url': {'url': image_url}}, - ] - result = openrouter_run(version, messages, callback_data, self.title) - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - output_tokens=result[2], - image=image - ) - msgs = self.save_results(result[0], process_time) - return msgs - - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({'role': 'assistant', 'content': content}) - else: - memory.append({'role': 'user', 'content': content}) - character_length = sum(len(content['content']) for content in memory) - while character_length > max_character_limit: - memory.pop(0) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -101,13 +101,18 @@ class Iconic(SimpleService): PRICE = Decimal('1.078') - _CALLBACK = 'miike-ai/flux-ico:478cae37f1aec0fde7977fdd54b272aaeabede7d8060801841920c16306369a9' + _CALLBACK = ( + 'miike-ai/flux-ico' + ':478cae37f1aec0fde7977fdd54b272aaeabede7d8060801841920c16306369a9' + ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -64,7 +64,7 @@ class Kandinsky(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, input_prompt: str, r: list[str], t: timedelta, save: bool = True + self, input_prompt: str, r: list[str], t: timedelta, save: bool = True ) -> list[Message]: out = [] for link in r: @@ -88,16 +88,16 @@ class Kandinsky(SimpleService): callback_data = dict( prompt=activation_prompt, negative_prompt=( - 'any form of nudity, sexual content, explicit or suggestive themes, ' - 'graphic violence, disturbing imagery, offensive symbols, hate speech, abusive language, ' - 'discriminatory content, illegal activities, or any form of inappropriate or harmful material. ' - 'This includes, but is not limited to, full or partial nudity, suggestive body imagery, sexual innuendos, ' - 'pornographic content, sexual acts, and anything that could be perceived as sexual or inappropriate. ' - 'Also, avoid any form of graphic violence, torture, gore, blood, or injury depiction. ' - 'Do not include offensive symbols, hate speech, racial or ethnic slurs, or any material promoting hatred or discrimination. ' - 'Any content promoting illegal activities, substance abuse, self-harm, or violence is strictly prohibited. ' - 'Furthermore, avoid any content that is offensive, harmful, inappropriate for minors, or unsuitable for a general audience. ' - f'Additionally, {negative_prompt} should be strictly avoided in any generated material.' + "any form of nudity, sexual content, explicit or suggestive themes, " + "graphic violence, disturbing imagery, offensive symbols, hate speech, abusive language, " + "discriminatory content, illegal activities, or any form of inappropriate or harmful material. " + "This includes, but is not limited to, full or partial nudity, suggestive body imagery, sexual innuendos, " + "pornographic content, sexual acts, and anything that could be perceived as sexual or inappropriate. " + "Also, avoid any form of graphic violence, torture, gore, blood, or injury depiction. " + "Do not include offensive symbols, hate speech, racial or ethnic slurs, or any material promoting hatred or discrimination. " + "Any content promoting illegal activities, substance abuse, self-harm, or violence is strictly prohibited. " + "Furthermore, avoid any content that is offensive, harmful, inappropriate for minors, or unsuitable for a general audience. " + f"Additionally, {negative_prompt} should be strictly avoided in any generated material." ), **input_message.info, ) @@ -86,14 +86,17 @@ class Lightning(SimpleService): PRICE = Decimal('0.825') _CALLBACK = ( - 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' + 'bytedance/sdxl-lightning-4step' + ':5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -4,6 +4,7 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -14,13 +15,42 @@ class Llama(SimpleService): contains abstract method make, which makes a generation """ + title = 'LLaMA' + description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' + category = ModelCategory(title='Чат-боты', slug='chat-bots') + versions = [ModelVersion(name='LLaMA2 70B', slug='llama2-70b', default=True)] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT)] + parameters = [ + ModelParameter( + name='Лучший процент', + key='top_p', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ModelParameter( + name='Лучший коэффициент', + key='top_k', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ModelParameter( + name='Температура', + key='temperature', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ] + price = Decimal('1.078') + model_version = '' def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, r: Iterator[Any], t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=''.join(word for word in r), @@ -36,17 +66,17 @@ class Llama(SimpleService): info = input_message.info context_ids: list[str] = info.pop('context_messages', []) content = ( - ''.join( - msg.content + ' ' - for msg in Message.objects.filter(uid__in=context_ids, chats_chats_messages=self.store) - ) - + input_message.content + ''.join( + msg.content + ' ' + for msg in Message.objects.filter( + uid__in=context_ids, chats_chats_messages=self.store + ) + ) + + input_message.content ) version = info.pop('version', 'llama2-70b') if version == 'llama2-70b': - self.model_version = ( - 'meta/llama-2-70b-chat:35042c9a33ac8fd5e29e27fb3197f33aa483f72c2ce3b0b9d201155c7fd2a287' - ) + self.model_version = 'meta/llama-2-70b-chat:35042c9a33ac8fd5e29e27fb3197f33aa483f72c2ce3b0b9d201155c7fd2a287' callback_data = dict( { 'prompt': content, @@ -7,6 +7,7 @@ import requests from django.core.files import File from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -17,15 +18,83 @@ class Logoai(SimpleService): contains abstract method make, which makes a generation """ + title = 'Logo AI' + description = 'Нейросеть, способная генерировать фотографии из вашего текста' + category = ModelCategory(title='Изображения', slug='images') + versions = [] + inputs = [ + ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE), + ] + parameters = [ + ModelParameter( + name='Негативный промпт', + key='negative_prompt', + type=ModelParameter.TypeChoices.STR, + ), + ModelParameter( + name='Ширина', + key='width', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1024, 'end': 2048, 'step': 128, 'default': 1024}, + ), + ModelParameter( + name='Высота', + key='height', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1024, 'end': 2048, 'step': 128, 'default': 1024}, + ), + ModelParameter( + name='Планировщик', + key='scheduler', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': [ + 'DDIM', + 'DPMSolverMultistep', + 'HeunDiscrete', + 'KarrasDPM', + 'K_EULER_ANCESTRAL', + 'K_EULER', + 'PNDM', + ], + 'default': 'K_EULER', + }, + ), + ModelParameter( + name='Количество изображений', + key='num_outputs', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='Точность запроса', + key='guidance_scale', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 50.0, 'step': 1.0, 'default': 7.5}, + ), + ModelParameter( + name='Шаги предобработки', + key='num_inference_steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 500, 'step': 1, 'default': 50}, + ), + ] + PRICE = Decimal('0.544') - _CALLBACK = 'mejiabrayan/logoai:67ed00e8999fecd32035074fa0f2e9a31ee03b57a8415e6a5e2f93a242ddd8d2' + _CALLBACK = ( + 'mejiabrayan/logoai' + ':67ed00e8999fecd32035074fa0f2e9a31ee03b57a8415e6a5e2f93a242ddd8d2' + ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -7,6 +7,7 @@ import requests from django.core.files import File from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -17,8 +18,36 @@ class Midjourney(SimpleService): contains abstract method make, which makes a generation """ - _CALLBACK = 'minimax/image-01' + title = 'Midjourney' + description = 'Нейросеть, способная генерировать фотографии из вашего текста' price = Decimal('2') + category = ModelCategory(title='Изображения', slug='images') + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] + parameters = [ + ModelParameter( + name='Соотношение сторон', + key='aspect_ratio', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'], + 'default': '1:1', + }, + ), + ModelParameter( + name='Количество изображений', + key='number_of_images', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 9, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='Оптимизация промпта', + key='prompt_optimizer', + type=ModelParameter.TypeChoices.BOOL, + values={'default': True}, + ), + ] + + _CALLBACK = 'minimax/image-01' def __init__(self, store): super().__init__(store) @@ -51,6 +80,8 @@ class Midjourney(SimpleService): callback_data = dict(prompt=activation_prompt, **input_message.info) results = replicate_run(self._CALLBACK, callback_data) process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, input_message=input_message) + self.handle_invoice( + input_message.content_object.model, input_message=input_message + ) msgs = self.save_results(input_message.content, results, process_time, save) return msgs @@ -52,11 +52,15 @@ class MinIOService: if bucket not in self.buckets: raise Exception(_('Unknown bucket destination')) - result = self.minio.get_presigned_url('GET', bucket, obj, expires=timedelta(days=7)) + result = self.minio.get_presigned_url( + 'GET', bucket, obj, expires=timedelta(days=7) + ) return result - def get_object(self, bucket: str, obj: str, as_BytesIO: bool = False) -> bytes | BytesIO: + def get_object( + self, bucket: str, obj: str, as_BytesIO: bool = False + ) -> bytes | BytesIO: if bucket not in self.buckets: raise Exception(_('Unknown bucket destination')) @@ -1,12 +1,22 @@ +import base64 import time -from _decimal import Decimal +import filetype + +from decimal import Decimal from datetime import timedelta -from typing import Any, Iterator +from io import BytesIO +from PIL import Image -from messages.models import Message -from ml_model.models import NeuronModel from ml_model.services.base import SimpleService -from ml_model.tasks import mistral_run +from ml_model.tasks import openrouter_run + +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + +from django.db.models.fields.files import FieldFile + +from messages.models import Message class Mistral(SimpleService): @@ -15,66 +25,114 @@ class Mistral(SimpleService): contains abstract method make, which makes a generation """ - TOKEN_PAYMENT_RULES = { - 'mistral-small-input': Decimal(0.706), - 'mistral-small-output': Decimal(2.123), + TOKENS_COST = { + 'mistral-small-3.1-24b-instruct': { + 'input': Decimal('20'), + 'output': Decimal('60'), + 'input_imgs': Decimal('185.2'), + }, } - def __init__(self, store): - super().__init__(store) - - @property - def neuron_model(self): - return NeuronModel.objects.get(title='Mistral') - - def calculate_price(self, messages: list[Message], input_message: Message) -> Decimal: - price = Decimal( - sum( - [ - self.TOKEN_PAYMENT_RULES[f'{input_message.info["version"]}-output'] - / 1000 - * len(msg.content.split(' ')) - for msg in messages - ] - + [ - self.TOKEN_PAYMENT_RULES[f'{input_message.info["version"]}-input'] - / 1000 - * len(input_message.content.split(' ')) - ] - ) + def calculate_price(self, version: str, input_tokens: int, output_tokens: int, image: FieldFile) -> Decimal: + price_map = self.TOKENS_COST[version.split('/')[1]] + price = ( + input_tokens * price_map['input'] / 1_000_000 + + output_tokens * price_map['output'] / 1_000_000 ) + if image: + price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def make_messages(self, r: Iterator[Any], t: timedelta): - msgs = [] - for message in r: - msgs.append( - Message( - content=message['message']['content'], - content_object=self.store, - elapsed_time=t, - ) + def save_results( + self, content: str, t: timedelta, save: bool = True + ) -> list[Message]: + msgs = [ + Message( + content=content, + content_object=self.store, + elapsed_time=t, ) + ] + if save: + return Message.objects.bulk_create(msgs) return msgs - def save_results(self, messages: list[Message]) -> list[Message]: - return Message.objects.bulk_create(messages) - def make(self, input_message: Message, save: bool = True) -> list[Message]: info = input_message.info.copy() - version = info.pop('version') - callback_data = dict( - { - 'messages': [{'role': 'user', 'content': input_message.content}], - 'model': version, - **info, - } - ) + version = f'mistralai/{info.pop('version')}' + callback_data = { + 'provider': { + 'order': ['Parasail'] + }, + **input_message.info + } + messages = self.get_chat_history() + image = input_message.file + if image: + kind = filetype.guess(image.read(20)) + mime = kind.mime if kind else 'application/octet-stream' + normalized_image = Image.open(image) + format = 'jpeg' if kind.extension == 'jpg' else kind.extension + buf = BytesIO() + normalized_image.save(buf, format=format) + image_url = ( + f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + ) + buf.close() + messages[-1]['content'] = [ + { + 'type': 'text', + 'text': input_message.content + }, + { + 'type': 'image_url', + 'image_url': { + 'url': image_url + } + } + ] start_time = time.time() - result = mistral_run(callback_data) + result = openrouter_run(version, messages, callback_data, self.title) process_time = timedelta(seconds=(time.time() - start_time)) - msgs = self.make_messages(result['choices'], process_time) - self.handle_invoice(self.neuron_model, messages=msgs, input_message=input_message) - if save: - self.save_results(messages=msgs) + self.handle_invoice( + input_message.content_object.model, + version=version, + input_tokens=result[1], + output_tokens=result[2], + image=image + ) + msgs = self.save_results(result[0], process_time) return msgs + + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + if isinstance(self.store, Chat): + air_messages = list( + reversed( + Message.objects.filter( + chats_chats_messages=self.store, is_deleted=False, is_sent=True + ).order_by('-created_at')[:message_limit] + ) + ) + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] + ) + ) + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({'role': 'assistant', 'content': content}) + else: + memory.append({'role': 'user', 'content': content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + character_length -= len(memory.pop(0)) + return memory @@ -7,6 +7,7 @@ import requests from django.core.files.base import File from messages.models import BaseStore, Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -17,14 +18,46 @@ class Musicgen(SimpleService): contains abstract method make, which makes a generation """ - _CALLBACK = 'meta/musicgen:7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906' + title = 'MusicGen' + description = 'Нейросеть, способная генерировать музыку из ваших слов' + price = Decimal('0.633') + category = ModelCategory(title='Чат-боты', slug='chat-bots') + versions = [ + ModelVersion(name='Melody', slug='melody', default=True), + ModelVersion(name='Large', slug='large'), + ] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT)] + parameters = [ + ModelParameter( + name='Лучший процент', + key='top_p', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ModelParameter( + name='Температура', + key='temperature', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ModelParameter( + name='Длительность', + key='duration', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 10, 'step': 1, 'default': 5}, + ), + ] + + _CALLBACK = ( + 'meta/musicgen:7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906' + ) def __init__(self, store: BaseStore): super().__init__(store) def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, r: str, t: timedelta, save: bool = True) -> list[Message]: out: list[Message] = [ @@ -1,103 +0,0 @@ -import time -from datetime import timedelta -from decimal import Decimal -from typing import Iterator, Any, Dict - -from messages.models import Message -from ml_model.models import ModelCategory, ModelVersion, ModelInput, ModelParameter -from ml_model.services.base import SimpleService -from ml_model.tasks import openrouter_run -from tools.chats.models import Chat -from tools.copywrite.models import Copywrite -from tools.public_api.models import APIStore - - -class Perplexity(SimpleService): - """ - Perplexity Service - contains abstract method make, which makes a generation - """ - - TOKENS_COST = { - 'sonar': {'input': Decimal('200'), 'output': Decimal('200')}, # 1M tokens - } - - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int - ) -> Decimal: - price_map = self.TOKENS_COST[version.split('/')[1]] - price = ( - input_tokens * price_map['input'] / 1_000_000 - + output_tokens * price_map['output'] / 1_000_000 - ) - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - - def save_results( - self, content: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] - if save: - return Message.objects.bulk_create(msgs) - return msgs - - def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - version = f'perplexity/{input_message.info.pop('version', 'sonar')}' - callback_data = { - 'provider': { - 'order': ['Perplexity'] - }, - **input_message.info - } - messages = self.get_chat_history() - result = openrouter_run(version, messages, callback_data, self.title) - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - output_tokens=result[2] - ) - msgs = self.save_results(result[0], process_time) - return msgs - - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({"role": "assistant", "content": content}) - else: - memory.append({"role": "user", "content": content}) - character_length = sum(len(content['content']) for content in memory) - while character_length > max_character_limit: - memory.pop(0) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -90,13 +90,17 @@ class Pulid(SimpleService): PRICE = Decimal('0.374') - _CALLBACK = 'zsxkib/pulid:43d309c37ab4e62361e5e29b8e9e867fb2dcbcec77ae91206a8d95ac5dd451a0' + _CALLBACK = ( + 'zsxkib/pulid:43d309c37ab4e62361e5e29b8e9e867fb2dcbcec77ae91206a8d95ac5dd451a0' + ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -1,93 +0,0 @@ -import time -from datetime import timedelta -from decimal import Decimal -from typing import Any, Iterator, Dict - -from messages.models import Message -from ml_model.services.base import SimpleService -from ml_model.tasks import openrouter_run -from tools.chats.models import Chat -from tools.copywrite.models import Copywrite -from tools.public_api.models import APIStore - - -class Qwen(SimpleService): - """ - Qwen Service - contains abstract method make, which makes a generation - """ - - TOKENS_COST = { - 'qwq-32b': {'input': Decimal('36'), 'output': Decimal('36')}, # 1M tokens - 'qwq-32b:free': {'input': Decimal('0'), 'output': Decimal('0')}, # 1M tokens - } - - def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: - price_map = self.TOKENS_COST[version.split('/')[1]] - price = ( - input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 - ) - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - - def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] - if save: - return Message.objects.bulk_create(msgs) - return msgs - - def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - version = f'qwen/{input_message.info.pop("version", "qwq-32b")}' - callback_data = {'provider': {'order': ['DeepInfra']}, **input_message.info} - messages = self.get_chat_history() - result = openrouter_run(version, messages, callback_data, self.title) - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - output_tokens=result[2], - ) - msgs = self.save_results(result[0], process_time) - return msgs - - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({'role': 'assistant', 'content': content}) - else: - memory.append({'role': 'user', 'content': content}) - character_length = sum(len(content['content']) for content in memory) - while character_length > max_character_limit: - memory.pop(0) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -6,7 +6,8 @@ from io import BytesIO import requests from django.core.files import File -from messages.models import Message +from backend import settings +from messages.models import BaseStore, Message from ml_model.models import ( ModelCategory, ModelInput, @@ -27,7 +28,7 @@ class Recraft(SimpleService): description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [ - ModelVersion(name='Recraft V3', slug='recraft-v3'), + ModelVersion(name='Recraft V3', slug='recraft-v3', default=True), ModelVersion(name='Recraft V3 SVG', slug='recraft-v3-svg'), ] inputs = [ @@ -70,7 +71,7 @@ class Recraft(SimpleService): 'естественное освещение', 'студийный портрет', 'предпринимательство', - 'размытие движения', + 'размытие движения' ], 'default': 'любой', }, @@ -99,21 +100,9 @@ class Recraft(SimpleService): def _get_size(self, width: int, height: int) -> str: available_sizes = ( - (1024, 1024), - (1365, 1024), - (1024, 1365), - (1536, 1024), - (1024, 1536), - (1820, 1024), - (1024, 1820), - (1024, 2048), - (2048, 1024), - (1434, 1024), - (1024, 1434), - (1024, 1280), - (1280, 1024), - (1024, 1707), - (1707, 1024), + (1024, 1024), (1365, 1024), (1024, 1365), (1536, 1024), (1024, 1536), + (1820, 1024), (1024, 1820), (1024, 2048), (2048, 1024), (1434, 1024), + (1024, 1434), (1024, 1280), (1280, 1024), (1024, 1707), (1707, 1024) ) if width >= height: size = min(available_sizes, key=lambda size: abs(width - size[0])) @@ -125,12 +114,12 @@ class Recraft(SimpleService): return self.payment_rules[input_message.info.get('version', 'recraft-v3')] def save_results( - self, - prompt: str, - image: str, - extension: str, - time: timedelta, - save: bool = True, + self, + prompt: str, + image: str, + extension: str, + time: timedelta, + save: bool = True, ) -> list[Message]: messages: list[Message] = [] messages.append( @@ -169,17 +158,12 @@ class Recraft(SimpleService): 'гравировка': 'engraving', 'контурный рисунок': 'line_art', 'схема': 'line_circuit', - 'линогравюра': 'linocut', + 'линогравюра': 'linocut' } start_time = time.time() - extension = ( - '.svg' if input_message.info.get('version', 'recraft-v3') == self.versions[1].slug else '.png' - ) - size = self._get_size( - input_message.info.pop('width', 1024), - input_message.info.pop('height', 1024), - ) - callback_url = f'recraft-ai/{input_message.info.get("version", "recraft-v3")}' + extension = '.svg' if input_message.info.get('version', 'recraft-v3') == self.versions[1].slug else '.png' + size = self._get_size(input_message.info.pop('width', 1024), input_message.info.pop('height', 1024)) + callback_url = f'recraft-ai/{input_message.info.get('version', 'recraft-v3')}' callback_data = dict( { 'prompt': ( @@ -188,7 +172,7 @@ class Recraft(SimpleService): ), 'size': size, 'style': styles.get(input_message.info.pop('style'), 'любой'), - **input_message.info, + **input_message.info } ) image = replicate_run(callback_url, callback_data) @@ -83,13 +83,17 @@ class Sdxlemoji(SimpleService): PRICE = Decimal('0.529') - _CALLBACK = 'fofr/sdxl-emoji:dee76b5afde21b0f01ed7925f0665b7e879c50ee718c5f78a9d38e04d523cc5e' + _CALLBACK = ( + 'fofr/sdxl-emoji:dee76b5afde21b0f01ed7925f0665b7e879c50ee718c5f78a9d38e04d523cc5e' + ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, prompt: str, images: list, time: timedelta, save: bool = True + ) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -22,11 +22,10 @@ class Stablediffusion(SimpleService): contains abstract method make, which makes a generation """ - MODELS = ['sd3', 'sd3-turbo', 'sd3-medium'] + MODELS = ['sd3', 'sd3-turbo'] MODELS_LINKS = { 'sd3': 'stable-diffusion-3.5-large', 'sd3-turbo': 'stable-diffusion-3.5-large-turbo', - 'sd3-medium': 'stable-diffusion-3.5-medium' } def calculate_price(self, input_message: Message) -> Decimal: @@ -34,8 +33,6 @@ class Stablediffusion(SimpleService): return Decimal('13') elif input_message.info.get('version') == 'sd3-turbo': return Decimal('8') - elif input_message.info.get('version') == 'sd3-medium': - return Decimal('7') def save_results( self, @@ -66,7 +66,10 @@ class Upscaleai(SimpleService): ), ] - _CALLBACK = 'mcai/babes-v2.0-img2img:2bca10ed539cf2196f18b4ec85128a80355d94934db8620884ecca552cdc4def' + _CALLBACK = ( + 'mcai/babes-v2.0-img2img' + ':2bca10ed539cf2196f18b4ec85128a80355d94934db8620884ecca552cdc4def' + ) def __init__(self, store): super().__init__(store) @@ -76,11 +79,7 @@ class Upscaleai(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, - input_prompt: str, - results: list[str], - t: timedelta, - save: bool = True, + self, input_prompt: str, results: list[str], t: timedelta, save: bool = True ) -> list[Message]: out: list[Message] = [] if len(results) > 1: @@ -91,7 +90,7 @@ class Upscaleai(SimpleService): content = requests.get(link).content except BaseException: continue - zipped.writestr(f'{uuid.uuid4()}_{link.split("/")[-1]}', content) + zipped.writestr(f"{uuid.uuid4()}_{link.split('/')[-1]}", content) zipped.close() out.append( Message( @@ -125,10 +124,7 @@ class Upscaleai(SimpleService): if input_message.file.name.split('.')[-1] == 'zip': results = upscale_run( dict( - archive=( - input_message.file.name, - BytesIO(input_message.file.read()), - ), + archive=(input_message.file.name, BytesIO(input_message.file.read())), prompt=(None, activation_prompt), upscale=(None, input_message.info.get('upscale', '0')), ) @@ -4,6 +4,7 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -14,16 +15,48 @@ class Vicuna(SimpleService): contains abstract method make, which makes a generation """ + title = 'Vicuna' + description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' + price = Decimal('0.886') + category = ModelCategory(title='Чат-боты', slug='chat-bots') + versions = [ModelVersion(name='13Billion', slug='13B', default=True)] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] + parameters = [ + ModelParameter( + name='Температура', + key='temperature', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ModelParameter( + name='Штраф за присутствие', + key='repetition_penalty', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ModelParameter( + name='Лучший процент', + key='top_p', + type=ModelParameter.TypeChoices.FLOATRANGE, + values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, + ), + ] + _CALLBACK = 'replicate/vicuna-13b:6282abe6a492de4145d7bb601023762212f9ddbbe78278bd6771c8b3b2f2a13b' def __init__(self, store): super().__init__(store) def calculate_price(self, messages: list[Message]) -> Decimal: - price = sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) * self.price + price = ( + sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) + * self.price + ) return price.quantize(Decimal('.01')) - def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, r: Iterator[Any], t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=''.join(word for word in r), @@ -8,6 +8,7 @@ from mutagen.mp3 import MP3 from mutagen.wave import WAVE from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import transcript_audio @@ -18,6 +19,14 @@ class Whisper(SimpleService): contains abstract method make, which makes a generation """ + title = 'Whisper' + description = 'Система автоматического распознавания голоса, обученная на 680000 часах аудио разных языков' + price = Decimal('0.088') # Per second > 4.8/min + category = ModelCategory(title='Аудио', slug='audio') + versions = [ModelVersion(name='Standart', slug='whisper-1', default=True)] + inputs = [ModelInput(type=ModelInput.TypeChoices.AUDIO)] + parameters = [] + def make(self, input_message: Message, save: bool = True) -> list[Message]: audio = BytesIO(input_message.file.read()) start_time = datetime.now() @@ -44,7 +53,9 @@ class Whisper(SimpleService): price = Decimal(length) * self.price return price.quantize(Decimal('.01')) - def save_results(self, r: str, file: File, t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, r: str, file: File, t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=r, @@ -1,13 +1,6 @@ -import re -import xml.etree.ElementTree as ET -from io import BytesIO -from tempfile import NamedTemporaryFile - from django.contrib import admin -from django.db.models.fields.files import FieldFile -from django.forms import ModelForm from django.http.response import HttpResponse as HttpResponse -from import_export.admin import ExportActionModelAdmin, ImportExportMixin +from import_export.admin import ImportExportMixin from ordered_model.admin import ( OrderedInlineModelAdminMixin, OrderedModelAdmin, @@ -23,18 +16,10 @@ from ml_model.models import ( ModelPaymentRule, ModelSettings, ModelStat, - ModelTag, ModelVersion, NeuronModel, ) -from ml_model.resources import ( - ModelCategoryResource, - ModelInputResource, - ModelParameterResource, - ModelTagResource, - ModelVersionResource, - NeuronModelResource, -) +from ml_model.resources import NeuronModelResource class ModelSettingsInline(admin.TabularInline): @@ -68,13 +53,7 @@ class ModelVersionsInline(OrderedTabularInline): model = ModelVersion extra = 0 classes = ['collapse'] - fields = ( - 'name', - 'description', - 'slug', - 'order', - 'move_up_down_links', - ) + fields = ('name', 'description', 'slug', 'default', 'order', 'move_up_down_links') readonly_fields = ('order', 'move_up_down_links') ordering = ('order',) @@ -87,17 +66,17 @@ class ModelStatInline(admin.TabularInline): @admin.register(ModelCategory) -class ModelCategoryAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): +class ModelCategoryAdmin(admin.ModelAdmin): list_display = ['title', 'slug'] + list_display_links = ('title', 'slug') prepopulated_fields = {'slug': ('title',)} - resource_classes = [ModelCategoryResource] @admin.register(NeuronModel) -class NeuronModelAdmin( - OrderedInlineModelAdminMixin, ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin +class NeuronModelModelAdmin( + OrderedInlineModelAdminMixin, ImportExportMixin, OrderedModelAdmin ): - list_display = ['title', '_active', '_category', 'move_up_down_links'] + list_display = ['title', 'is_active', 'category', 'move_up_down_links'] resource_classes = (NeuronModelResource,) prepopulated_fields = {'slug': ('title',)} inlines = [ @@ -108,83 +87,16 @@ class NeuronModelAdmin( ModelInputsInline, ModelParametersInline, ] - list_filter = ['model_settings__is_active', 'category', 'tags'] - - filter_horizontal = ['tags'] + list_filter = ['model_settings__is_active', 'category__title'] @admin.display(description='Активна?', boolean=True) - def _active(self, obj: NeuronModel): - return obj.active + def is_active(self, obj: NeuronModel): + if obj.settings: + return obj.settings.is_active - @admin.display(description='Категория') - def _category(self, obj: NeuronModel): - if obj.category: - return obj.category.title - return 'Не присвоена' - - -@admin.register(ModelTag) -class ModelTagAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): - list_display = ['title', 'slug'] - prepopulated_fields = {'slug': ['title']} - resource_classes = [ModelTagResource] - - def save_model(self, request, obj: ModelTag, form: ModelForm, change): - if not isinstance(form.cleaned_data['icon'], FieldFile) and obj.icon.readable(): - with NamedTemporaryFile() as f: - obj.icon.seek(0) - content = obj.icon.read() - f.write(content) - f.seek(0) - root = ET.parse(f.name).getroot() - f.seek(0) - for element in root.iter(): - tag_regexp = re.match(r'{.*}(?P.*)', element.tag) - if tag_regexp: - element.tag = tag_regexp.groupdict().get('tagname') - for attr in ['width', 'height']: - if attr in element.attrib: - del element.attrib[attr] - for attr, val in { - 'fill': 'currentColor', - 'stroke': 'currentColor', - }.items(): - if element.tag == 'svg' and attr == 'stroke': - continue - if attr in element.attrib: - element.attrib[attr] = val - buf = BytesIO() - ET.ElementTree(root).write( - buf, encoding='utf-8', xml_declaration=True, default_namespace={}.get(None) - ) - buf.seek(0) - from django.core.files import File - - obj.icon.save( - form.cleaned_data['icon'].name, - File(buf, form.cleaned_data['icon'].name), - ) - return super().save_model(request, obj, form, change) - - -@admin.register(ModelVersion) -class ModelVersionAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): - resource_classes = [ModelVersionResource] - list_filter = ['model'] - - -@admin.register(ModelInput) -class ModelInputAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): - resource_classes = [ModelInputResource] - filter_horizontal = ['versions'] - list_filter = ('model', 'versions') - - -@admin.register(ModelParameter) -class ModelParameterAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): - resource_classes = [ModelParameterResource] - filter_horizontal = ['versions'] - list_filter = ('model', 'versions') + @admin.display(description='Категория', boolean=True) + def category(self, obj: NeuronModel): + return obj.category.title class ConfigurationParameterInline(admin.TabularInline): @@ -192,7 +104,7 @@ class ConfigurationParameterInline(admin.TabularInline): extra = 1 can_delete = False - def has_change_permission(self, *args, **kwargs): + def has_change_permission(self, request, obj=...): return False @@ -1,5 +1,4 @@ from django.apps import AppConfig -from django.core.signals import setting_changed from django.utils.translation import gettext_lazy as _ @@ -7,10 +6,3 @@ class MLModelConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'ml_model' verbose_name = _('Neuron Models') - - def ready(self): - from .signals import create_settings - - setting_changed.connect(create_settings) - - return super().ready() @@ -2,15 +2,12 @@ from uuid import uuid4 from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.contrib.postgres.fields import ArrayField -from django.core.validators import FileExtensionValidator from django.db import models from django.db.models import F, QuerySet from django.utils.translation import gettext_lazy as _ from django_minio_backend.models import MinioBackend from ordered_model.models import OrderedModel, OrderedModelManager -from core.fields import ColorField from core.models import BaseModel @@ -30,44 +27,13 @@ class ModelCategory(models.Model): verbose_name_plural = _('Categories') -def model_tag_icon_uploader(instance: 'ModelTag', filename: str): - return f'tags/{instance.slug}/{filename}' - - -class ModelTag(models.Model): - title = models.CharField(max_length=100, verbose_name=_('Title')) - slug = models.SlugField(max_length=120, verbose_name=_('Slug'), unique=True) - color = ColorField(verbose_name=_('Color'), null=True, blank=True) - icon = models.FileField( - storage=MinioBackend('air-models'), - upload_to=model_tag_icon_uploader, - validators=[FileExtensionValidator(['svg'], _('Not SVG-pictures not allowed'))], - blank=True, - null=True, - verbose_name=_('Icon'), - ) - - def __str__(self): - return self.title - - class Meta: - verbose_name = _('Model Tag') - verbose_name_plural = _('Model Tags') - - -def upload_model_avatar(instance: 'NeuronModel', filename: str): +def upload_model_avatar(instance, filename): return f'avatars/{instance.slug}/{filename}' class NeuronModel(BaseModel, OrderedModel): title = models.CharField(max_length=300, verbose_name=_('Title')) - alternative_titles = ArrayField( - models.CharField(max_length=30), - default=list, - blank=True, - verbose_name=_('Alternative Titles'), - ) - description = models.TextField(verbose_name=_('Description'), null=True, blank=True) + description = models.TextField(max_length=4000, verbose_name=_('Description')) slug = models.SlugField( verbose_name=_('Slug'), unique=True, @@ -77,8 +43,6 @@ class NeuronModel(BaseModel, OrderedModel): category = models.ForeignKey( 'ModelCategory', on_delete=models.PROTECT, - null=True, - blank=True, verbose_name=_('Category'), related_name='category_models', ) @@ -90,8 +54,6 @@ class NeuronModel(BaseModel, OrderedModel): verbose_name=_('Avatar'), ) - tags = models.ManyToManyField(ModelTag, blank=True, verbose_name=_('Tags'), related_name='models_tags') - objects = OrderedModelManager() order_with_respect_to = 'category' @@ -130,13 +92,9 @@ class NeuronModel(BaseModel, OrderedModel): except ModelSettings.DoesNotExist: return None - @property - def active(self) -> bool: - return bool(self.settings) and self.settings.is_active - @property def blocked(self) -> bool: - return not self.active + return self.settings and not self.settings.is_active def __str__(self): return self.title @@ -159,12 +117,17 @@ class ModelDepends(models.Model): class ModelSettings(models.Model): - model = models.OneToOneField(NeuronModel, on_delete=models.CASCADE, related_name='model_settings') + model = models.OneToOneField( + NeuronModel, on_delete=models.CASCADE, related_name='model_settings' + ) is_active = models.BooleanField( - default=False, + default=True, verbose_name=_('Is active'), help_text='Модель активна для всех пользователей', ) + authorization_token = models.CharField( + null=True, blank=True, max_length=255, verbose_name=_('Authorization token') + ) class Meta: verbose_name = _('Settings') @@ -176,8 +139,11 @@ class ModelSettings(models.Model): class ModelVersion(ModelDepends, OrderedModel): name = models.CharField(max_length=16, verbose_name=_('Name')) - description = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Description')) - slug = models.CharField(max_length=32, verbose_name=_('Slug')) + description = models.CharField( + max_length=128, null=True, blank=True, verbose_name=_('Description') + ) + slug = models.CharField(max_length=32, unique=True, verbose_name=_('Slug')) + default = models.BooleanField(default=False, verbose_name=_('Default')) order_with_respect_to = 'model' @@ -250,10 +216,7 @@ class ModelParameter(ModelDepends, ModelVersionsDepends, OrderedModel): INT = 'int', _('Integer') # Integer FLOAT = 'float', _('Float') # Float STR = 'str', _('String') # String - LIST = ( - 'list', - _('List'), - ) # That accepts a lot of values, that may changed + LIST = 'list', _('List') # That accepts a lot of values, that may changed FLOATRANGE = ( 'floatrange', _('Float range'), @@ -265,7 +228,9 @@ class ModelParameter(ModelDepends, ModelVersionsDepends, OrderedModel): BOOL = 'bool', _('Logical') # True/False name = models.CharField(verbose_name=_('Title'), max_length=50) - description = models.CharField(verbose_name=_('Description'), null=True, blank=True, max_length=512) + description = models.CharField( + verbose_name=_('Description'), null=True, blank=True, max_length=512 + ) key = models.CharField(verbose_name=_('Key'), max_length=40) type = models.CharField( verbose_name=_('Type'), @@ -276,7 +241,9 @@ class ModelParameter(ModelDepends, ModelVersionsDepends, OrderedModel): blank=True, default=dict, verbose_name=_('Values'), - help_text=_('These values can contain different interfaces and default value optional'), + help_text=_( + 'These values can contain different interfaces and default value optional' + ), ) hidden = models.BooleanField(_('Hidden'), default=False) required = models.BooleanField(_('Required'), default=False) @@ -304,9 +271,7 @@ class ModelPaymentRule(ModelDepends, ModelVersionsDepends): ALL = 'all', _('By all data') strategy = models.CharField( - max_length=32, - choices=StrategyChoices.choices, - verbose_name=_('Strategy'), + max_length=32, choices=StrategyChoices.choices, verbose_name=_('Strategy') ) interaction_type = models.CharField( max_length=32, @@ -340,7 +305,9 @@ class ModelPaymentRule(ModelDepends, ModelVersionsDepends): class ModelStat(ModelDepends): generation_time = models.DurationField(verbose_name='Время генерации') - tokens_cost = models.DecimalField(max_digits=50, decimal_places=10, verbose_name='Цена в токенах') + tokens_cost = models.DecimalField( + max_digits=50, decimal_places=10, verbose_name='Цена в токенах' + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name='Когда создано') class Meta: @@ -350,7 +317,9 @@ class ModelStat(ModelDepends): class ModelConfiguration(models.Model): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') + id = models.UUIDField( + primary_key=True, default=uuid4, editable=False, verbose_name='ID' + ) model = models.ForeignKey( NeuronModel, on_delete=models.PROTECT, @@ -358,6 +327,15 @@ class ModelConfiguration(models.Model): related_name='model_configurations', verbose_name='Модель', ) + version = models.ForeignKey( + ModelVersion, + on_delete=models.PROTECT, + to_field='slug', + null=True, + blank=True, + related_name='version_configurations', + verbose_name='Версия модели', + ) ct = models.ForeignKey(ContentType, on_delete=models.CASCADE) oid = models.UUIDField() @@ -1,112 +1,76 @@ from import_export import fields as ie_fields from import_export.resources import ModelResource -from import_export.widgets import ForeignKeyWidget, ManyToManyWidget +from import_export.widgets import ForeignKeyWidget from ml_model.models import ( ModelCategory, ModelInput, ModelParameter, - ModelPaymentRule, - ModelTag, ModelVersion, NeuronModel, ) - - -class ModelTagResource(ModelResource): - class Meta: - model = ModelTag - exclude = ('id', 'color', 'icon') - import_id_fields = ('slug',) - - -class ModelCategoryResource(ModelResource): - class Meta: - model = ModelCategory - exclude = ('id',) - import_id_fields = ('slug',) +from ml_model.serializers import ( + ModelInputSerializer, + ModelParameterSerializer, + ModelVersionSerializer, +) class NeuronModelResource(ModelResource): category = ie_fields.Field( - column_name='category', + column_name='Категория', attribute='category', + readonly=True, widget=ForeignKeyWidget(ModelCategory, 'slug'), ) - tags = ie_fields.Field( - column_name='tags', - attribute='tags', - widget=ManyToManyWidget(ModelTag, ',', 'slug'), - ) - - class Meta: - model = NeuronModel - exclude = ('uid', 'order', 'created_at', 'updated_at', 'image', 'description') - import_id_fields = ('slug',) - - -class ModelVersionResource(ModelResource): - model = ie_fields.Field( - column_name='model', - attribute='model', - widget=ForeignKeyWidget(NeuronModel, 'slug'), - ) - - class Meta: - model = ModelVersion - exclude = ('id', 'description') - import_id_fields = ('model', 'slug') - - -class ModelInputResource(ModelResource): - model = ie_fields.Field( - column_name='model', - attribute='model', - widget=ForeignKeyWidget(NeuronModel, 'slug'), - ) versions = ie_fields.Field( - column_name='versions', - attribute='versions', - widget=ManyToManyWidget(ModelVersion, ',', 'slug'), + column_name='Версии', ) - - class Meta: - model = ModelInput - exclude = ('id',) - import_id_fields = ('model', 'versions', 'type') - - -class ModelParameterResource(ModelResource): - model = ie_fields.Field( - column_name='model', - attribute='model', - widget=ForeignKeyWidget(NeuronModel, 'slug'), + inputs = ie_fields.Field( + column_name='Входящие потоки', ) - versions = ie_fields.Field( - column_name='versions', - attribute='versions', - widget=ManyToManyWidget(ModelVersion, ',', 'slug'), + parameters = ie_fields.Field( + column_name='Параметры', ) - class Meta: - model = ModelParameter - exclude = ('id',) - import_id_fields = ('model', 'versions', 'key') - - -class ModelPaymentRuleResource(ModelResource): - model = ie_fields.Field( - column_name='model', - attribute='model', - widget=ForeignKeyWidget(NeuronModel, 'slug'), - ) - versions = ie_fields.Field( - column_name='versions', - attribute='versions', - widget=ManyToManyWidget(ModelVersion, ',', 'slug'), - ) + def dehydrate_versions(self, obj: NeuronModel): + return ModelVersionSerializer(obj.versions, many=True).data + + def dehydrate_inputs(self, obj: NeuronModel): + return ModelInputSerializer(obj.inputs, many=True).data + + def dehydrate_parameters(self, obj: NeuronModel): + return ModelParameterSerializer(obj.parameters, many=True).data + + def after_init_instance(self, instance, new, row, **kwargs): + raw_versions = row.pop('versions', []) + for raw_version in raw_versions: + ModelVersion.objects.get_or_create( + model=instance, slug=raw_version.pop('slug'), defaults=raw_version + ) + raw_inputs = row.pop('inputs', []) + for raw_input in raw_inputs: + depends_on_versions = raw_input.pop('versions', []) + input, _ = ModelInput.objects.get_or_create( + model=instance, type=raw_input.pop('type'), defaults=raw_input + ) + input.versions.set(ModelVersion.objects.filter(slug__in=depends_on_versions)) + raw_parameters = row.pop('parameters', []) + for raw_parameter in raw_parameters: + depends_on_versions = raw_parameter.pop('versions', []) + parameter, _ = ModelParameter.objects.get_or_create( + model=instance, + key=raw_parameter.pop('key'), + values=raw_parameter.pop('values'), + defaults=raw_parameter, + ) + parameter.versions.set( + ModelVersion.objects.filter(slug__in=depends_on_versions) + ) + return super().after_init_instance(instance, new, row, **kwargs) class Meta: - model = ModelPaymentRule - exclude = ('id',) - import_id_fields = ('model', 'versions', 'strategy', 'interaction_type') + model = NeuronModel + use_transactions = True + exclude = ('uid', 'order', 'created_at', 'updated_at', 'description') + import_id_fields = ('slug',) @@ -22,7 +22,7 @@ class ModelConfigurationSchema(ModelSchema): class Meta: model = ModelConfiguration - exclude = ('ct', 'oid', 'obj', 'model') + exclude = ('ct', 'oid', 'obj', 'model', 'version') class Config: protected_namespaces = () @@ -31,4 +31,4 @@ class ModelConfigurationSchema(ModelSchema): class NeuronModelLink(ModelSchema): class Meta: model = NeuronModel - fields = ('title', 'slug', 'alternative_titles') + fields = ('title', 'slug') @@ -5,7 +5,6 @@ from ml_model.models import ( ModelInput, ModelParameter, ModelSettings, - ModelTag, ModelVersion, NeuronModel, ) @@ -14,7 +13,7 @@ from ml_model.models import ( class ModelSettingsSerializer(serializers.ModelSerializer): class Meta: model = ModelSettings - exclude = ('id', 'model') + exclude = ('id', 'model', 'authorization_token') class ModelVersionSerializer(serializers.ModelSerializer): @@ -39,31 +38,23 @@ class ModelInputSerializer(serializers.ModelSerializer): exclude = ('id', 'model') -class ModelTagSerializer(serializers.ModelSerializer): - class Meta: - model = ModelTag - exclude = ('id', 'slug') - - class NeuronModelSerializer(serializers.ModelSerializer): + settings = ModelSettingsSerializer() parameters = ModelParameterSerializer(many=True) versions = ModelVersionSerializer(many=True) inputs = ModelInputSerializer(many=True) - tags = ModelTagSerializer(many=True) - blocked = serializers.BooleanField() class Meta: model = NeuronModel - exclude = ('created_at', 'updated_at', 'order', 'category', 'alternative_titles') + exclude = ('created_at', 'updated_at', 'order', 'category') class NeuronModelsSerializer(serializers.ModelSerializer): blocked = serializers.BooleanField() - tags = ModelTagSerializer(many=True) class Meta: model = NeuronModel - exclude = ('created_at', 'updated_at', 'order', 'category', 'alternative_titles', 'uid') + exclude = ('created_at', 'updated_at', 'order', 'category', 'uid') class ModelCategorySerializer(serializers.ModelSerializer): @@ -1,12 +0,0 @@ -from typing import Type - -from django.db.models.signals import post_save -from django.dispatch import receiver - -from ml_model.models import ModelSettings, NeuronModel - - -@receiver(post_save, sender=NeuronModel) -def create_settings(sender: Type[NeuronModel], instance: NeuronModel, created: bool, **kwargs): - if created: - ModelSettings.objects.create(model=instance) @@ -1,13 +1,12 @@ import base64 import json -import logging +import httpx # import uuid from io import BytesIO from typing import IO, Any, Dict import deepl -import httpx import replicate import requests from celery import shared_task @@ -17,15 +16,13 @@ from requests import Response from backend import settings from poller.models import Proxy -logger = logging.getLogger(__name__) - @shared_task def create_d_image(payload: dict): if payload.get('image'): return json.loads( requests.post( - f'http://{settings.OPENAI_PROXY_HOST}?{"&".join([f"proxies={proxy.protocol}://{proxy.address}" for proxy in Proxy.objects.all()])}&uri=images/variations&token={settings.OPENAI_API_KEY}', + f"http://{settings.OPENAI_PROXY_HOST}?{'&'.join([f'proxies={proxy.protocol}://{proxy.address}' for proxy in Proxy.objects.all()])}&uri=images/variations&token={settings.OPENAI_API_KEY}", json=payload, timeout=(600, 600), headers={ @@ -35,7 +32,7 @@ def create_d_image(payload: dict): ) return json.loads( requests.post( - f'http://{settings.OPENAI_PROXY_HOST}?{"&".join([f"proxies={proxy.protocol}://{proxy.address}" for proxy in Proxy.objects.all()])}&uri=images/generations&token={settings.OPENAI_API_KEY}', + f"http://{settings.OPENAI_PROXY_HOST}?{'&'.join([f'proxies={proxy.protocol}://{proxy.address}' for proxy in Proxy.objects.all()])}&uri=images/generations&token={settings.OPENAI_API_KEY}", json=payload, timeout=(600, 600), headers={ @@ -48,7 +45,7 @@ def create_d_image(payload: dict): @shared_task(serializer='pickle') def create_sd_image(api_key: str, payload: dict, **kwargs) -> list[tuple[BytesIO, str]]: response = requests.post( - f'https://api.stability.ai/v1/generation/{payload.pop("engine")}/text-to-image', + f"https://api.stability.ai/v1/generation/{payload.pop('engine')}/text-to-image", headers={ 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -57,7 +54,10 @@ def create_sd_image(api_key: str, payload: dict, **kwargs) -> list[tuple[BytesIO json=dict(text_prompts=[{'text': payload.pop('prompt')}], **payload), ) if response.status_code == 200: - return [BytesIO(base64.b64decode(gen['base64'])) for gen in response.json()['artifacts']] + return [ + BytesIO(base64.b64decode(gen['base64'])) + for gen in response.json()['artifacts'] + ] else: raise Exception(response.json()) @@ -94,7 +94,7 @@ def translate(payload: dict[str, Any]) -> Response | TextResult: def transcript_audio(payload: dict[str, Any]): return json.loads( requests.post( - f'http://{settings.OPENAI_PROXY_HOST}?{"&".join([f"proxies={proxy.protocol}://{proxy.address}" for proxy in Proxy.objects.all()])}&uri=audio/transcript&token={settings.OPENAI_API_KEY}', + f"http://{settings.OPENAI_PROXY_HOST}?{'&'.join([f'proxies={proxy.protocol}://{proxy.address}' for proxy in Proxy.objects.all()])}&uri=audio/transcript&token={settings.OPENAI_API_KEY}", json=payload, timeout=(600, 600), headers={ @@ -112,30 +112,38 @@ def replicate_run(callback_url: str, payload: dict[str, Any]): input=payload, ) - @shared_task def openrouter_run(version: str, messages: list, callback_data: dict, model_name: str): for proxy in Proxy.objects.all(): with httpx.Client( - base_url='https://openrouter.ai/api/v1', - headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, - proxy=f'{proxy.protocol}://{proxy.address}', - timeout=600, + base_url='https://openrouter.ai/api/v1', + headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, + proxy=f'{proxy.protocol}://{proxy.address}', ) as client: resp = client.post( 'chat/completions', - json={'model': version, 'messages': messages, **callback_data}, + json={ + 'model': version, + 'messages': messages, + **callback_data + }, ) if ( - (data := resp.json()) - and data.get('choices') - and (content := ','.join([choice['message']['content'] for choice in data.get('choices')])) + (data := resp.json()) + and data.get('choices') + and ( + content := ','.join( + [choice['message']['content'] for choice in data.get('choices')] + ) + ) ): - return (content, data['usage']['prompt_tokens'], data['usage']['completion_tokens']) - logger.error(f'Error occured via model {model_name}. Data: {resp.content}') + return ( + content, + data['usage']['prompt_tokens'], + data['usage']['completion_tokens'] + ) raise Exception(f'No answer from {model_name}, please retry later') - @shared_task def upscale_run(payload: dict[str, tuple[str, IO]]) -> list[str]: content = requests.post( @@ -144,22 +152,6 @@ def upscale_run(payload: dict[str, tuple[str, IO]]) -> list[str]: ).content return json.loads(content) - -@shared_task -def mistral_run(payload: dict[str, Any]): - headers = { - 'Content-Type': 'application/json', - 'Authorization': settings.MISTRAL_API_KEY, - } - return json.loads( - requests.post( - 'https://api.mistral.ai/v1/chat/completions', - json.dumps(payload), - headers=headers, - ).content - ) - - @shared_task def claude_run(payload: dict[str, Any]): headers = { @@ -169,9 +161,7 @@ def claude_run(payload: dict[str, Any]): } return json.loads( requests.post( - 'https://api.anthropic.com/v1/messages', - json.dumps(payload), - headers=headers, + 'https://api.anthropic.com/v1/messages', json.dumps(payload), headers=headers ).content ) @@ -1,10 +1,6 @@ from django.urls import path -from ml_model.views import ( - CategoriesAPIView, - NeuronModelAPIView, - NeuronModelsAPIView, -) +from ml_model.views import CategoriesAPIView, NeuronModelAPIView, NeuronModelsAPIView app_name = 'ml_model' @@ -2,12 +2,8 @@ from random import randint from typing import Literal from authentication.models import CustomUserModel -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) -from authentication.selectors.business_account_selector import ( - BusinessAccountSelector, -) +from authentication.selectors.account_status_selector import AccountStatusSelector +from authentication.selectors.business_account_selector import BusinessAccountSelector def random_with_N_digits(n): @@ -56,6 +56,8 @@ class NeuronModelAPIView(APIView): """Retrieve model by slug""" return Response( NeuronModelSerializer( - NeuronModelSelector(request.user).get_model_by_slug(slug=slug, hidden=False) + NeuronModelSelector(request.user).get_model_by_slug( + slug=slug, hidden=False + ) ).data ) @@ -16,11 +16,7 @@ yookassa_payments_total.extend(yookassa_payments.items) while yookassa_payments.next_cursor: yookassa_payments = YookassaPayment.list( - params={ - 'status': 'succeeded', - 'limit': 100, - 'cursor': yookassa_payments.next_cursor, - } + params={'status': 'succeeded', 'limit': 100, 'cursor': yookassa_payments.next_cursor} ) yookassa_payments_total.extend(yookassa_payments.items) @@ -11,10 +11,7 @@ class Command(BaseCommand): user.referral_code = PromoCode.objects.create( promocode_type=PromoCode.REFERRAL, owner=user, - function_call={ - 'name': 'add_tokens_referral', - 'arguments': {'amount': 5}, - }, + function_call={'name': 'add_tokens_referral', 'arguments': {'amount': 5}}, ) user.save() return 'Done!' @@ -49,7 +49,9 @@ class Payment(BaseModel): null=False, blank=False, ) - description = models.CharField(verbose_name=_('Description'), max_length=128, blank=True, null=True) + description = models.CharField( + verbose_name=_('Description'), max_length=128, blank=True, null=True + ) def __str__(self): return f'{self.uid}' @@ -24,7 +24,9 @@ class PaymentPlan(BaseModel): ) is_corporate = models.BooleanField(default=False, verbose_name=_('Is corporate')) is_recurrent = models.BooleanField(default=False, verbose_name=_('Is recurrent')) - title = models.CharField(verbose_name=_('Title'), max_length=120, null=True, blank=True) + title = models.CharField( + verbose_name=_('Title'), max_length=120, null=True, blank=True + ) duration = models.CharField( verbose_name=_('Duration'), max_length=100, @@ -82,11 +84,7 @@ class PaymentPlanUserInfo(BaseModel): ) def save( - self, - force_insert=False, - force_update=False, - using=None, - update_fields=None, + self, force_insert=False, force_update=False, using=None, update_fields=None ): self.last_payment_at = datetime.now().date() self.next_payment_at = self.last_payment_at + relativedelta(months=1) @@ -38,10 +38,7 @@ class PromoCode(BaseModel): unique=True, ) promocode_type = models.CharField( - verbose_name=_('Type'), - choices=PROMOCODE_TYPES, - null=False, - blank=False, + verbose_name=_('Type'), choices=PROMOCODE_TYPES, null=False, blank=False ) function_call = models.JSONField( @@ -82,7 +79,9 @@ class PromoCode(BaseModel): help_text=_('Can be activated only one time'), ) - is_active = models.BooleanField(verbose_name=_('Is active'), blank=True, null=False, default=True) + is_active = models.BooleanField( + verbose_name=_('Is active'), blank=True, null=False, default=True + ) def clean(self): if self.promocode_type != PromoCode.REFERRAL and self.owner: @@ -104,7 +103,9 @@ class PromoCodeActivation(BaseModel): on_delete=models.CASCADE, verbose_name=_('Activated by'), ) - promocode = models.ForeignKey(to=PromoCode, on_delete=models.CASCADE, verbose_name=_('Promocode')) + promocode = models.ForeignKey( + to=PromoCode, on_delete=models.CASCADE, verbose_name=_('Promocode') + ) def save(self, *args, **kwargs) -> None: if self._state.adding: @@ -12,8 +12,12 @@ class UserPaymentMethod(BaseModel): related_name='payment_methods', verbose_name='Пользователь', ) - currently_active = models.BooleanField(default=False, verbose_name='Способ платежа активен') - payment_method_id = models.UUIDField(unique=True, verbose_name='UID платёжного метода') + currently_active = models.BooleanField( + default=False, verbose_name='Способ платежа активен' + ) + payment_method_id = models.UUIDField( + unique=True, verbose_name='UID платёжного метода' + ) card_type = models.CharField(max_length=15, verbose_name='Тип карты') last_four = models.CharField(max_length=4, verbose_name='Последние 4 цифры карты') @@ -10,10 +10,7 @@ from authentication.models.choices import InvitationStatus from authentication.models.user import CustomUserModel from authentication.selectors.user_selector import UserSelector from payments.models.payment_plan import PaymentPlan -from payments.serializers import ( - PaymentPlanSerializer, - UserPaymentPlanSerializer, -) +from payments.serializers import PaymentPlanSerializer, UserPaymentPlanSerializer logger = logging.getLogger(__name__) @@ -90,8 +87,12 @@ class PaymentPlanSelector: return UserPaymentPlanSerializer(plan) - def get_free_plan(self, corporate: bool = False, recurrent: bool = False) -> PaymentPlan: - return PaymentPlan.objects.get_or_create(price=0, is_corporate=corporate, is_recurrent=recurrent)[0] + def get_free_plan( + self, corporate: bool = False, recurrent: bool = False + ) -> PaymentPlan: + return PaymentPlan.objects.get_or_create( + price=0, is_corporate=corporate, is_recurrent=recurrent + )[0] def is_plan_paid(self) -> bool: return self.user.payment_plan.plan.price != Decimal('0') @@ -23,16 +23,10 @@ class PaymentSelector(BaseSelector): return payments @classmethod - def count_by_utm( - cls, - utm: UTM, - from_date: date | None = None, - to_date: date | None = None, - ): + def count_by_utm(cls, utm: UTM, from_date: date | None = None, to_date: date | None = None): if from_date and to_date: return Payment.objects.filter( - created_at__date__range=[from_date, to_date], - user__in=utm.users.all(), + created_at__date__range=[from_date, to_date], user__in=utm.users.all() ).count() elif from_date: return Payment.objects.filter(created_at__date__gte=from_date, user__in=utm.users.all()).count() @@ -41,17 +35,11 @@ class PaymentSelector(BaseSelector): return Payment.objects.filter(user__in=utm.users.all()).count() @classmethod - def sum_by_utm( - cls, - utm: UTM, - from_date: date | None = None, - to_date: date | None = None, - ): + def sum_by_utm(cls, utm: UTM, from_date: date | None = None, to_date: date | None = None): if from_date and to_date: float( Payment.objects.filter( - created_at__date__range=[from_date, to_date], - user__in=utm.users.all(), + created_at__date__range=[from_date, to_date], user__in=utm.users.all() ).aggregate(Sum('amount', default=0))['amount__sum'] ) elif from_date: @@ -16,11 +16,7 @@ class ModelBillingService: if user_type in ['regular', 'business_host']: plan = self.user.payment_plan allowance = None - elif user_type in [ - 'business_account', - 'business_admin', - 'business_security', - ]: + elif user_type in ['business_account', 'business_admin', 'business_security']: plan = self.user.business_account.parent_company.user.payment_plan allowance = ( self.user.business_account.token_limit @@ -6,9 +6,7 @@ from rest_framework.request import Request from authentication.models import CustomUserModel from payments.models import Invoice, PaymentPlan, PaymentPlanUserInfo from payments.selectors.payment_plan_selector import PaymentPlanSelector -from payments.selectors.recurrent_payment_selector import ( - RecurrentPaymentSelector, -) +from payments.selectors.recurrent_payment_selector import RecurrentPaymentSelector from payments.serializers import PaymentLinkSerializer, SuccessPaymentResult from payments.services.model_billing_service import ModelBillingService from payments.services.payment_service import PaymentService @@ -39,7 +37,9 @@ class PaymentPlanService: def create_payment_plan_invoice(self, payment_plan_uid: str): """""" - payment_plan = PaymentPlanSelector(self.user).get_payment_plan_by_id(payment_plan_uid) + payment_plan = PaymentPlanSelector(self.user).get_payment_plan_by_id( + payment_plan_uid + ) resulting_link = self.payment_service.create_payment_link(plan=payment_plan) result = PaymentLinkSerializer(data={'payment_url': resulting_link}) result.is_valid(raise_exception=True) @@ -52,15 +52,14 @@ class PaymentPlanService: plan.plan_schedule = task.task plan.save() else: - current.to_service(RecurrentPaymentService).switch_plan(self.user, plan.plan.uid) + current.to_service(RecurrentPaymentService).switch_plan( + self.user, plan.plan.uid + ) def subscribe_user_to_plan(self, plan: PaymentPlan): plan_info, created = PaymentPlanUserInfo.objects.get_or_create( user=self.user, - defaults={ - 'plan': plan, - 'current_token_balance': plan.tokens_per_plan, - }, + defaults={'plan': plan, 'current_token_balance': plan.tokens_per_plan}, ) if not created: plan_info.plan = plan @@ -72,13 +71,17 @@ class PaymentPlanService: task = plan_info.plan_schedule if task is not None: RecurrentPaymentService(task).delete() - plan_info.plan = PaymentPlanSelector(self.user).get_free_plan(corporate=self.user.is_corporate()) + plan_info.plan = PaymentPlanSelector(self.user).get_free_plan( + corporate=self.user.is_corporate() + ) plan_info.save() def update_per_token_plan_details(self, payment_amount: Decimal, model=None): ModelBillingService(self.user).charge(payment_amount) if model: - return Invoice.objects.create(model=model, user=self.user, cost=payment_amount) + return Invoice.objects.create( + model=model, user=self.user, cost=payment_amount + ) def refill_user_plan_details(self): payment_plan = self.user.payment_plan @@ -79,15 +79,16 @@ class PaymentService: try: if user.referer_account: ReferralAccountService.apply_accrual( - referer_account=user.referer_account, - payment=payment_instance, + referer_account=user.referer_account, payment=payment_instance ) except Exception as exc: logger.exception(exc) return payment_instance @classmethod - def save_payment(cls, user: CustomUserModel, payment: YookassaPayment) -> PaymentModel: + def save_payment( + cls, user: CustomUserModel, payment: YookassaPayment + ) -> PaymentModel: payment_instance, _ = PaymentModel.objects.update_or_create( uid=payment.id, defaults=dict( @@ -33,8 +33,5 @@ class PromoCodeService(BaseService): return PromoCode.objects.create( promocode_type=PromoCode.REFERRAL, owner=self.user, - function_call={ - 'name': 'add_tokens_referral', - 'arguments': {'amount': 5}, - }, + function_call={'name': 'add_tokens_referral', 'arguments': {'amount': 5}}, ) @@ -16,11 +16,15 @@ class ReferralAccountService: @classmethod def create_invite(cls, referer_account: CustomUserModel, invitee: CustomUserModel): - return ReferralInvite.objects.create(referer_account=referer_account, invitee=invitee) + return ReferralInvite.objects.create( + referer_account=referer_account, invitee=invitee + ) @classmethod def apply_accrual(cls, referer_account: ReferralAccount, payment: Payment): - accrual_amount = (payment.plan.tokens_per_plan * Decimal(0.2)).quantize(Decimal('1')) + accrual_amount = (payment.plan.tokens_per_plan * Decimal(0.2)).quantize( + Decimal('1') + ) accrual = ReferralAccrual.objects.create( referer_account=referer_account, payment=payment, @@ -157,14 +157,18 @@ class InvoiceAdmin(admin.ModelAdmin): @admin.register(PromoCode) class PromoCodeAdmin(admin.ModelAdmin): list_display = ['code', 'promocode_type', 'function_call'] - search_fields = [*(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields)] + search_fields = [ + *(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields) + ] raw_id_fields = ['activated_by', 'owner'] @admin.register(PromoCodeActivation) class PromoCodeActivationAdmin(admin.ModelAdmin): list_display = ['_code', 'activated_by', 'created_at'] - search_fields = [*(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields)] + search_fields = [ + *(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields) + ] raw_id_fields = ['activated_by'] @admin.display(description='Промокод') @@ -232,7 +236,9 @@ class AccruedTokensFilter(admin.SimpleListFilter): ] def queryset(self, request, queryset): - queryset = queryset.annotate(total_bonuses=Sum('account_referral_accruals__amount')) + queryset = queryset.annotate( + total_bonuses=Sum('account_referral_accruals__amount') + ) match self.value(): case 'more-zero': return queryset.filter(total_bonuses__gt=0) @@ -251,11 +257,7 @@ class ReferralAccountAdmin(admin.ModelAdmin): '_accrued_bonuses', ) inlines = (ReferralInviteInline,) - list_filter = ( - RegistrationsCountFilter, - PaymentsCountFilter, - AccruedTokensFilter, - ) + list_filter = (RegistrationsCountFilter, PaymentsCountFilter, AccruedTokensFilter) @admin.display(description='Приглашающий') def _owner(self, obj: ReferralAccount): @@ -9,10 +9,7 @@ from payments.services.referral_account import ReferralAccountService @receiver(post_save, sender=CustomUserModel) def init_referral_account( - sender: Type[CustomUserModel], - instance: CustomUserModel, - created: bool, - **kwargs, + sender: Type[CustomUserModel], instance: CustomUserModel, created: bool, **kwargs ): if created: ReferralAccountService.create_account(user=instance) @@ -12,11 +12,7 @@ urlpatterns = [ ), path('invoices', views.InvoicesAPIView.as_view(), name='invoices'), path('plans', views.PaymentPlanAPIView.as_view(), name='payment-plans'), - path( - 'methods', - views.PaymentMethodsAPIView.as_view(), - name='payment-methods', - ), + path('methods', views.PaymentMethodsAPIView.as_view(), name='payment-methods'), path('user-balance', views.UserPlanAPIView.as_view(), name='user-balance'), path( 'payment-result', @@ -6,11 +6,7 @@ from django.db.models import F, Sum, functions from django.db.models.query import QuerySet from django.db.transaction import atomic from django.utils import timezone -from drf_spectacular.utils import ( - OpenApiParameter, - OpenApiResponse, - extend_schema, -) +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework import status from rest_framework.generics import ListAPIView from rest_framework.permissions import IsAuthenticated @@ -61,22 +57,25 @@ class PaymentPlanAPIView(APIView): permission_classes = (IsAuthenticated, IsAllowedToPay) @extend_schema( - parameters=[OpenApiParameter('duration', str, enum=[PaymentPlan.MONTH, PaymentPlan.YEAR])], + parameters=[ + OpenApiParameter('duration', str, enum=[PaymentPlan.MONTH, PaymentPlan.YEAR]) + ], responses={200: PaymentPlanSerializer}, ) def get(self, request: Request, *args, **kwargs): """List available payment plans""" try: plan_duration = request.query_params.get('duration', None) - result = PaymentPlanSelector(self.request.user).get_payment_plans(plan_duration=plan_duration) + result = PaymentPlanSelector(self.request.user).get_payment_plans( + plan_duration=plan_duration + ) return Response(result.data, status=status.HTTP_200_OK) except Exception as err: logger.exception(err) return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @extend_schema( - request=NewSubsriptionSerializer, - responses={200: PaymentLinkSerializer}, + request=NewSubsriptionSerializer, responses={200: PaymentLinkSerializer} ) def post(self, request, *args, **kwargs): """Create new payment link for chosen Payment Plan""" @@ -133,7 +132,9 @@ class PaymentMethodsAPIView(APIView): try: serializer = DeletePaymentMethodSerializer(data=request.data) serializer.is_valid(raise_exception=True) - PaymentMethodService(self.request.user).delete_payment_method(**serializer.validated_data) + PaymentMethodService(self.request.user).delete_payment_method( + **serializer.validated_data + ) return Response({'ok': True}, status=status.HTTP_200_OK) except Exception as err: logger.exception(err) @@ -243,13 +244,17 @@ class ExpensesAPIView(APIView): qs = qs.filter( created_at__range=[ timezone.now() - timedelta(days=timezone.now().weekday()), - timezone.now() - timedelta(days=timezone.now().weekday()) + timedelta(days=6), + timezone.now() + - timedelta(days=timezone.now().weekday()) + + timedelta(days=6), ] ) case 'previous_month': qs = qs.filter( created_at__range=[ - (timezone.now().replace(day=1) - timedelta(days=1)).replace(day=1), + (timezone.now().replace(day=1) - timedelta(days=1)).replace( + day=1 + ), timezone.now().replace(day=1) - timedelta(days=1), ] ) @@ -257,12 +262,10 @@ class ExpensesAPIView(APIView): qs = qs.filter( created_at__range=[ datetime.strptime( - request.query_params.get('from', '01.01.00'), - '%d.%m.%y', + request.query_params.get('from', '01.01.00'), '%d.%m.%y' ), datetime.strptime( - request.query_params.get('to', '01.01.50'), - '%d.%m.%y', + request.query_params.get('to', '01.01.50'), '%d.%m.%y' ), ] ) @@ -274,8 +277,7 @@ class ExpensesAPIView(APIView): ) case 'models': qs = qs.values('model__title').annotate( - source=F('model__title'), - amount=functions.Round((Sum('cost'))), + source=F('model__title'), amount=functions.Round((Sum('cost'))) ) case 'days': qs = qs.values(day=functions.TruncDay('created_at')).annotate( @@ -310,7 +312,9 @@ class PromoCodeAPIView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( - parameters=[OpenApiParameter(name='code', type=str, required=True, description='Промокод')], + parameters=[ + OpenApiParameter(name='code', type=str, required=True, description='Промокод') + ], responses={ 200: OpenApiResponse(description='Promocode sucessfully activated'), 403: OpenApiResponse(description='Promocode found, but inactive'), @@ -320,7 +324,9 @@ class PromoCodeAPIView(APIView): def post(self, request: Request, *args, **kwargs): """Activate promocde as authenticated user.""" try: - PromoCodeService(request.user).activate_by_code(code=request.data.get('code', '')) + PromoCodeService(request.user).activate_by_code( + code=request.data.get('code', '') + ) return Response(status=status.HTTP_200_OK) except PromoCodeService.PromoCodeInactive as e: return Response(status=status.HTTP_403_FORBIDDEN, data={'detail': str(e)}) @@ -336,10 +342,7 @@ class TelegramChannelSubscriptionBonusAPIView(APIView): @extend_schema( parameters=[ OpenApiParameter( - name='id', - type=str, - required=True, - description='Telegram ID of user.', + name='id', type=str, required=True, description='Telegram ID of user.' ) ], responses={ @@ -9,11 +9,7 @@ class Command(BaseCommand): help = 'Collect reports and create an Excel file' def add_arguments(self, parser): - parser.add_argument( - 'from_datetime', - type=str, - help='Start date and time (YYYY-MM-DD HH:MM:SS)', - ) + parser.add_argument('from_datetime', type=str, help='Start date and time (YYYY-MM-DD HH:MM:SS)') parser.add_argument( 'to_datetime', type=str, @@ -6,7 +6,9 @@ from core.models import BaseModel class ErrorReport(BaseModel): - author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_('Author')) + author = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, verbose_name=_('Author') + ) report_text = models.TextField(verbose_name=_('Text')) additional_images = models.JSONField(null=True, verbose_name=_('Attachments')) @@ -20,12 +20,7 @@ class RequestReponseLogAdmin(admin.ModelAdmin): search_fields = ['created_at', 'response_body'] verbose_name = 'Запрос и ответ' verbose_name_plural = 'Запросы и ответы' - list_display = [ - 'request_body', - 'request_user', - 'response_body', - 'status_code', - ] + list_display = ['request_body', 'request_user', 'response_body', 'status_code'] raw_id_fields = ['request_user'] actions = ['download_xlsx_logs'] @@ -16,6 +16,8 @@ class SendErrorReportAPIView(APIView): """Send new error report from form data.""" try: ErrorReportService(self.request.user).create(request) - return Response({'detail': 'error report sent'}, status=status.HTTP_201_CREATED) + return Response( + {'detail': 'error report sent'}, status=status.HTTP_201_CREATED + ) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -1,5 +1,6 @@ from typing import List +from django.db.models import Count from ninja import Router from authentication.security import SyncAuthBearer @@ -13,6 +14,6 @@ router = Router(auth=SyncAuthBearer(), tags=['chats']) def get_links(request): return ( NeuronModel.objects.filter(category__slug='chat-bots') - .filter(model_settings__isnull=False, model_settings__is_active=True) - .order_by('order') + .annotate(inputs_count=Count('model_modelinputs')) + .filter(inputs_count__gt=0) ) @@ -2,10 +2,7 @@ import logging import sys from drf_spectacular.utils import OpenApiParameter, extend_schema -from rest_framework.generics import ( - ListCreateAPIView, - RetrieveUpdateDestroyAPIView, -) +from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -169,7 +166,9 @@ class MessageAPIView(APIView): Put message into favourite """ chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get(uid=message_uid, chats_chats_messages=chat, is_deleted=False) + message = Message.objects.get( + uid=message_uid, chats_chats_messages=chat, is_deleted=False + ) message.is_favourite = True message.save() return Response(status=204) @@ -179,7 +178,9 @@ class MessageAPIView(APIView): Put message into shared """ chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get(uid=message_uid, chats_chats_messages=chat, is_deleted=False) + message = Message.objects.get( + uid=message_uid, chats_chats_messages=chat, is_deleted=False + ) message.is_shared = True message.save() return Response(status=204) @@ -192,7 +193,9 @@ class MessageAPIView(APIView): Hide message """ chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get(pk=message_uid, chats_chats_messages=chat, is_deleted=False) + message = Message.objects.get( + pk=message_uid, chats_chats_messages=chat, is_deleted=False + ) message.is_deleted = True message.save() return Response(status=204) @@ -7,7 +7,9 @@ from messages.models import MultipleStore class Chat(MultipleStore): title = models.CharField(max_length=50, verbose_name=_('Title')) - created_at = models.DateTimeField(default=timezone.now, verbose_name=_('Created at'), editable=False) + created_at = models.DateTimeField( + default=timezone.now, verbose_name=_('Created at'), editable=False + ) is_deleted = models.BooleanField( default=False, verbose_name=_('Is deleted'), @@ -6,7 +6,9 @@ from tools.chats.models import Chat class ChatCreateSerializer(serializers.ModelSerializer): user = serializers.HiddenField(default=serializers.CurrentUserDefault()) - model = serializers.SlugRelatedField(slug_field='slug', queryset=NeuronModel.objects.all()) + model = serializers.SlugRelatedField( + slug_field='slug', queryset=NeuronModel.objects.all() + ) class Meta: model = Chat @@ -85,18 +85,14 @@ def generate(request, id: UUID): @router.put( - 'copywrites/{id}/favourite', - tags=['copywrite/copywrites'], - response={204: None}, + 'copywrites/{id}/favourite', tags=['copywrite/copywrites'], response={204: None} ) def mark_as_favourite(request, id: UUID): return CopywriteService.mark_as_favourite(copywrite_id=id) @router.delete( - 'copywrites/{id}/favourite', - tags=['copywrite/copywrites'], - response={204: None}, + 'copywrites/{id}/favourite', tags=['copywrite/copywrites'], response={204: None} ) def remove_from_favourite(request, id: UUID): return CopywriteService.remove_from_favourite(copywrite_id=id) @@ -118,7 +114,9 @@ def delete_copywrite(request, id: UUID): response=OverridenVariableSchema, ) def override_variable(request, copywrite_id: UUID, data: CreateOverridenVariableSchema): - return CopywriteService.override_variable(copywrite_id=copywrite_id, **data.model_dump()) + return CopywriteService.override_variable( + copywrite_id=copywrite_id, **data.model_dump() + ) @router.put( @@ -126,7 +124,9 @@ def override_variable(request, copywrite_id: UUID, data: CreateOverridenVariable tags=['copywrite/variables'], response={204: None}, ) -def update_overriden_variable(request, copywrite_id: UUID, id: UUID, data: UpdateOverridenVariableSchema): +def update_overriden_variable( + request, copywrite_id: UUID, id: UUID, data: UpdateOverridenVariableSchema +): return CopywriteService.update_overriden_variable( copywrite_id=copywrite_id, variable_id=id, **data.model_dump() ) @@ -151,9 +151,7 @@ def list_template_categories(request): @router.get( - 'templates/', - tags=['copywrite/templates'], - response=List[TemplateShortSchema], + 'templates/', tags=['copywrite/templates'], response=List[TemplateShortSchema] ) def list_templates(request, filters: TemplateFilterSchema = Query(...)): return TemplateService.list_templates(filters=filters.get_filter_expression()) @@ -42,12 +42,16 @@ class CopywriteService: type: Literal['template', 'self'], initial: dict[str, Any], ) -> Copywrite: - klass = ContentType.objects.get_by_natural_key('copywrite', f'{type}copywrite').model_class() + klass = ContentType.objects.get_by_natural_key( + 'copywrite', f'{type}copywrite' + ).model_class() return klass.objects.create(user=user, **initial) @classmethod def get_copywrite_by_id(cls, copywrite_id: UUID) -> TemplateCopywrite | SelfCopywrite: - return Copywrite.objects.prefetch_related('polymorphic_ctype').get(id=copywrite_id) + return Copywrite.objects.prefetch_related('polymorphic_ctype').get( + id=copywrite_id + ) @classmethod def mark_as_favourite(cls, copywrite_id: UUID) -> None: @@ -77,25 +81,30 @@ class CopywriteService: def update_overriden_variable( cls, copywrite_id: UUID, variable_id: UUID, value: PrimitiveType ) -> OverridenVariable: - ov = OverridenVariable.objects.get(copywrite__id=copywrite_id, variable__id=variable_id) + ov = OverridenVariable.objects.get( + copywrite__id=copywrite_id, variable__id=variable_id + ) ov.value = value ov.save() return ov @classmethod def remove_overriden_variable(cls, copywrite_id: UUID, variable_id: UUID) -> None: - OverridenVariable.objects.get(copywrite__id=copywrite_id, variable__id=variable_id).delete() + OverridenVariable.objects.get( + copywrite__id=copywrite_id, variable__id=variable_id + ).delete() @classmethod def generate(cls, copywrite_id: UUID) -> Generator[str, None, None]: - cp: SelfCopywrite | TemplateCopywrite = Copywrite.objects.prefetch_related('polymorphic_ctype').get( - id=copywrite_id - ) + cp: SelfCopywrite | TemplateCopywrite = Copywrite.objects.prefetch_related( + 'polymorphic_ctype' + ).get(id=copywrite_id) if isinstance(cp, SelfCopywrite): content = str(cp.input_content) or '' elif isinstance(cp, TemplateCopywrite): overriden_vars = { - overriden_var.variable.id: overriden_var.value for overriden_var in cp.overriden_variables + overriden_var.variable.id: overriden_var.value + for overriden_var in cp.overriden_variables } blueprint = Template(cp.template.content) ctx = { @@ -13,7 +13,7 @@ class CopywriteConsumer(AsyncJsonWebsocketConsumer): channel_layer: RedisChannelLayer async def connect(self): - copywrite_id = f'{self.scope["url_route"]["kwargs"]["copywrite_id"]}' + copywrite_id = f"{self.scope['url_route']['kwargs']['copywrite_id']}" await self.channel_layer.group_add( COPYWRITE_WS_KEY % copywrite_id, self.channel_name, @@ -24,7 +24,7 @@ class CopywriteConsumer(AsyncJsonWebsocketConsumer): async def close(self, code=None, reason=None): await self.channel_layer.group_discard( - COPYWRITE_WS_KEY % f'{self.scope["url_route"]["kwargs"]["copywrite_id"]}', + COPYWRITE_WS_KEY % f"{self.scope['url_route']['kwargs']['copywrite_id']}", self.channel_name, ) return await super().close(code, reason) @@ -33,11 +33,15 @@ class TemplateCategory(OrderedModel): class Template(OrderedModel): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') + id = models.UUIDField( + primary_key=True, default=uuid4, editable=False, verbose_name='ID' + ) title = models.CharField(max_length=50, unique=True, verbose_name='Название') slug = models.SlugField(unique=True, verbose_name='Ярлык') - description = models.CharField(null=True, blank=True, max_length=200, verbose_name='Описание') + description = models.CharField( + null=True, blank=True, max_length=200, verbose_name='Описание' + ) picture = models.FileField( verbose_name='Картинка', storage=MinioBackend(bucket_name='air-templates-pictures'), @@ -83,7 +87,9 @@ class Template(OrderedModel): class TemplateVariable(OrderedModel): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') + id = models.UUIDField( + primary_key=True, default=uuid4, editable=False, verbose_name='ID' + ) template = models.ForeignKey( Template, @@ -96,7 +102,9 @@ class TemplateVariable(OrderedModel): name = models.CharField(max_length=100, verbose_name='Название') sysname = models.CharField(max_length=100, verbose_name='Системное название') required = models.BooleanField(default=False, verbose_name='Обязательный') - default_value = models.JSONField(null=True, blank=True, verbose_name='Стандартное значение') + default_value = models.JSONField( + null=True, blank=True, verbose_name='Стандартное значение' + ) order_with_respect_to = 'template' @@ -106,7 +114,9 @@ class TemplateVariable(OrderedModel): class Copywrite(PolymorphicModel): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') + id = models.UUIDField( + primary_key=True, default=uuid4, editable=False, verbose_name='ID' + ) user = models.ForeignKey( get_user_model(), on_delete=models.SET_NULL, @@ -115,7 +125,9 @@ class Copywrite(PolymorphicModel): verbose_name='Пользователь', related_name='user_copywrites', ) - output_content = models.TextField(null=True, blank=True, verbose_name='Исходящий промпт') + output_content = models.TextField( + null=True, blank=True, verbose_name='Исходящий промпт' + ) favourite = models.BooleanField(default=False, verbose_name='В избранном') created_at = models.DateTimeField(auto_now_add=True, verbose_name='Когда создано') deleted = models.BooleanField(default=False, verbose_name='Удален') @@ -131,7 +143,9 @@ class Copywrite(PolymorphicModel): @property def type(self) -> str: - return self.polymorphic_ctype.model[: self.polymorphic_ctype.model.index('copywrite')] + return self.polymorphic_ctype.model[ + : self.polymorphic_ctype.model.index('copywrite') + ] @property def generated(self) -> bool: @@ -153,7 +167,9 @@ class Copywrite(PolymorphicModel): class SelfCopywrite(Copywrite): - input_content: str = models.TextField(null=True, blank=True, verbose_name='Входящий промпт') + input_content: str = models.TextField( + null=True, blank=True, verbose_name='Входящий промпт' + ) class Meta: verbose_name = 'Самописный копирайт' @@ -178,7 +194,9 @@ class TemplateCopywrite(Copywrite): class OverridenVariable(models.Model): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') + id = models.UUIDField( + primary_key=True, default=uuid4, editable=False, verbose_name='ID' + ) variable = models.ForeignKey( TemplateVariable, on_delete=models.PROTECT, @@ -98,13 +98,7 @@ class SelfCopywriteSchema(BaseCopywriteSchema, ModelSchema): class Meta: model = SelfCopywrite - exclude = ( - 'copywrite_ptr', - 'user', - 'polymorphic_ctype', - 'created_at', - 'deleted', - ) + exclude = ('copywrite_ptr', 'user', 'polymorphic_ctype', 'created_at', 'deleted') class SelfCopywriteShortSchema(BaseCopywriteSchema, ModelSchema): @@ -144,13 +138,7 @@ class TemplateCopywriteSchema(BaseCopywriteSchema, ModelSchema): class Meta: model = TemplateCopywrite - exclude = ( - 'copywrite_ptr', - 'user', - 'polymorphic_ctype', - 'created_at', - 'deleted', - ) + exclude = ('copywrite_ptr', 'user', 'polymorphic_ctype', 'created_at', 'deleted') class TemplateCopywriteShortSchema(BaseCopywriteSchema, ModelSchema): @@ -174,7 +162,9 @@ class CopywriteFilterSchema(FilterSchema): def filter_types(self, value: List[Literal['template', 'self']]): if value: - return Q(polymorphic_ctype__model__in=list(map(lambda x: f'{x}copywrite', value))) + return Q( + polymorphic_ctype__model__in=list(map(lambda x: f'{x}copywrite', value)) + ) return Q() @@ -7,5 +7,7 @@ from tools.copywrite.models import TemplateCopywrite @receiver(pre_save, sender=TemplateCopywrite) -def copy_model_configuration(sender: Type[TemplateCopywrite], instance: TemplateCopywrite, **kwargs): +def copy_model_configuration( + sender: Type[TemplateCopywrite], instance: TemplateCopywrite, **kwargs +): print(kwargs) @@ -17,10 +17,7 @@ def generate(copywrite_id: UUID): for chunk in CopywriteService.generate(copywrite_id=copywrite_id): async_to_sync(layer.group_send)( COPYWRITE_WS_KEY % copywrite_id, - { - 'type': 'generate.chunk', - 'event_data': {'chunk': chunk, 'end': False}, - }, + {'type': 'generate.chunk', 'event_data': {'chunk': chunk, 'end': False}}, ) content += chunk cache.set(f'copywrite:{copywrite_id}', content) @@ -37,7 +37,7 @@ class Command(BaseCommand): file.write('from django.urls import path') with open(TOOL_DIR / 'apps.py', 'a') as apps: apps.write( - f'\n\nclass {options["toolname"][0].title()}Config(AppConfig):' + f"\n\nclass {options['toolname'][0].title()}Config(AppConfig):" "\n\tdefault_auto_field = 'django.db.models.BigAutoField'" f"\n\tname = 'tools.{options['toolname'][0]}'" ) @@ -1,5 +1,6 @@ from typing import List +from django.db.models import Count from ninja import Router from authentication.security import SyncAuthBearer @@ -9,14 +10,10 @@ from ml_model.schemas import NeuronModelLink router = Router(auth=SyncAuthBearer(), tags=['media']) -@router.get( - 'images/links/', - tags=['media/images/links'], - response=List[NeuronModelLink], -) +@router.get('images/links/', tags=['media/images/links'], response=List[NeuronModelLink]) def get_links(request): return ( NeuronModel.objects.filter(category__slug='images') - .filter(model_settings__isnull=False, model_settings__is_active=True) - .order_by('order') + .annotate(inputs_count=Count('model_modelinputs')) + .filter(inputs_count__gt=0) ) @@ -129,7 +129,7 @@ class MediaAPIView(APIView): info = serializer.validated_data.pop('info', {}) service: type[SimpleService] = getattr( sys.modules['ml_model.services'], - f'{gallery.model.slug.replace("-", "").title()}', + f"{gallery.model.slug.replace('-', '').title()}", ) input_message = Message.objects.create( **serializer.validated_data, @@ -1,11 +1,6 @@ from django.urls import path -from .apis import ( - GalleryAPIView, - ModelAudiosAPIVIew, - ModelImagesAPIView, - ModelVideosAPIView, -) +from .apis import GalleryAPIView, ModelAudiosAPIVIew, ModelImagesAPIView, ModelVideosAPIView urlpatterns = [ path('gallery/', GalleryAPIView.as_view(), name='gallery'), @@ -4,9 +4,7 @@ from decimal import Decimal from django.contrib.admin.models import ADDITION, DELETION, LogEntry from django.contrib.contenttypes.models import ContentType -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) +from authentication.selectors.account_status_selector import AccountStatusSelector from core.service import BaseService from tools.public_api.models import APIKey from tools.public_api.selectors.api_key import APIKeySelector @@ -14,7 +12,9 @@ from tools.public_api.serializers import APIKeyResultSerializer class APIKeyService(BaseService): - def create(self, payload: dict, serialize: bool = False) -> APIKey | APIKeyResultSerializer: + def create( + self, payload: dict, serialize: bool = False + ) -> APIKey | APIKeyResultSerializer: if not ( AccountStatusSelector(self.user).is_business_host() or AccountStatusSelector(self.user).is_admin() @@ -31,7 +31,7 @@ class APIKeyService(BaseService): { 'added': { 'name': 'API-ключ', - 'object': f'{api_key.key[:3]}{(len(api_key.key) - 4) * "*"}{api_key.key[len(api_key.key) - 4 :]}', + 'object': f'{api_key.key[:3]}{(len(api_key.key)-4)*"*"}{api_key.key[len(api_key.key)-4:]}', } } ], @@ -73,7 +73,7 @@ class APIKeyService(BaseService): { 'deleted': { 'name': 'API-ключ', - 'object': f'{api_key.key[:3]}{(len(api_key.key) - 4) * "*"}{api_key.key[len(api_key.key) - 4 :]}', + 'object': f'{api_key.key[:3]}{(len(api_key.key)-4)*"*"}{api_key.key[len(api_key.key)-4:]}', } } ], @@ -1,10 +1,3 @@ from .api_key import APIKeyView -from .ml_service import ( - TextView, - ImageView, - AudioView, - VideoView, - CodeView, - ParamView, -) +from .ml_service import TextView, ImageView, AudioView, VideoView, CodeView, ParamView from .user import UserInfoAPIView @@ -1,6 +1,5 @@ import sys -from rest_framework.exceptions import APIException from rest_framework.response import Response from rest_framework.views import APIView @@ -42,20 +41,24 @@ class BaseGenerationView(APIView): def post(self, request, model_slug, *args, **kwargs): """Create new content. Type of content depends on model output content type: text, image, audio, video, or code.""" - user = APIKeySelector.get_user_by_key(key_value=request.headers.get('Authorization', '')) + user = APIKeySelector.get_user_by_key( + key_value=request.headers.get('Authorization', '') + ) balance = user.balance key = APIKey.objects.get(key=request.headers.get('Authorization', '')) - if key.token_limit is not None and (key.token_limit > balance or key.token_limit < 0): + if key.token_limit is not None and ( + key.token_limit > balance or key.token_limit < 0 + ): return Response({'error': 'Not enough tokens on balance or key limit'}, 403) store, _ = APIStore.objects.get_or_create(user=user) - model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug(slug=model_slug) - if model.blocked: - raise APIException( - detail=_('Model is blocked by outdating or temporary block, please retry later') - ) + model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug( + slug=model_slug + ) serializer = MessageSerializer(data=request.data) serializer.is_valid(raise_exception=True) - service: type[SimpleService] = getattr(sys.modules['ml_model.services'], f'{model.slug.title()}') + service: type[SimpleService] = getattr( + sys.modules['ml_model.services'], f'{model.slug.title()}' + ) info = serializer.validated_data.pop('info', {}) input_message = Message( **serializer.validated_data, @@ -71,7 +74,9 @@ class BaseGenerationView(APIView): msg.from_public_api = True msg.save() if key.token_limit is not None: - user_after = APIKeySelector.get_user_by_key(key_value=request.headers.get('Authorization', '')) + user_after = APIKeySelector.get_user_by_key( + key_value=request.headers.get('Authorization', '') + ) key.token_limit -= balance - user_after.balance key.save() return Response(MessageSerializer(output_message, many=True).data, 201) @@ -46,8 +46,12 @@ class ParamView(APIView): @extend_schema(responses={200: ModelParameterSerializer}) def get(self, request, model_slug, *args, **kwargs): """List parameters for model pointed in URL Slug.""" - user = APIKeySelector.get_user_by_key(key_value=request.headers.get('Authorization', '')) + user = APIKeySelector.get_user_by_key( + key_value=request.headers.get('Authorization', '') + ) store, created = APIStore.objects.get_or_create(user=user) - model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug(slug=model_slug) + model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug( + slug=model_slug + ) params = ParamSelector.get_params_by_model(model, serialize=True) return Response(data=params.data, status=status.HTTP_200_OK) @@ -27,7 +27,9 @@ class APIKey(BaseModel): blank=False, default=generate_api_key, ) - name = models.CharField(verbose_name=_('Name'), max_length=255, null=False, blank=False) + name = models.CharField( + verbose_name=_('Name'), max_length=255, null=False, blank=False + ) user = models.ForeignKey( to=get_user_model(), verbose_name=_('Owner'), @@ -16,14 +16,7 @@ class APIKeyResultSerializer(serializers.ModelSerializer): class Meta: model = APIKey - fields = ( - 'created_at', - 'name', - 'key', - 'expires_at', - 'user', - 'token_limit', - ) + fields = ('created_at', 'name', 'key', 'expires_at', 'user', 'token_limit') class APIKeyCreateSerializer(serializers.ModelSerializer): @@ -6,20 +6,11 @@ urlpatterns = [ path('me', views.UserInfoAPIView.as_view()), ] -for view in ( - views.TextView, - views.ImageView, - views.AudioView, - views.VideoView, - views.CodeView, -): +for view in (views.TextView, views.ImageView, views.AudioView, views.VideoView, views.CodeView): urlpatterns.extend( [ path(view.output_content_type, view.as_view()), path(f'{view.output_content_type}/', view.as_view()), - path( - f'{view.output_content_type}//params', - views.ParamView.as_view(), - ), + path(f'{view.output_content_type}//params', views.ParamView.as_view()), ] ) @@ -3,7 +3,6 @@ SECRET_KEY=testtest DEBUG=true # NEURON MODELS -OPENROUTER_API_KEY=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 OPENAI_API_KEY=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 STABLE_DIFFUSION_API_KEY=sk-fztQxZobaL0SD7PgpmK7XMQlyNivpKFZNJnqAVG2CcbvAP6Z REPLICATE_API_KEY=r8_HBk6Ts5UJU60nDOUl1V6Uej4ihAAxUc3HAZLO @@ -11,25 +11,16 @@ default: before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin -build_staging: +build: stage: Build + image: docker:latest script: - touch .env && export ENV=.env - - docker compose build - - docker compose push - only: - - staging - when: on_success - -build_production: - stage: Build - script: - - touch .env && export ENV=.env - - docker rmi $CI_REGISTRY_IMAGE:latest || true - docker compose -f stack.yml build - docker compose -f stack.yml push only: - main + - staging when: on_success deploy_staging: @@ -14,7 +14,7 @@ services: python manage.py collectstatic --no-input python manage.py compilemessages (python manage.py createsuperuser --no-input || true) - python -m debugpy --listen 0.0.0.0:5678 -m uvicorn backend.asgi:application --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level debug --reload + python -m debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn backend.asgi:application --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level debug --reload volumes: - .:/code ports: @@ -1,10 +1,11 @@ services: - app: + backend: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA build: context: . dockerfile: Dockerfile + container_name: backend volumes: - static:/code/static command: @@ -14,17 +15,6 @@ services: python manage.py collectstatic --no-input python manage.py compilemessages python -m uvicorn --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level info backend.asgi:application - labels: - - traefik.enable=true - - traefik.docker.network=infrastructure - - traefik.http.routers.backend.rule=Host(`$DOMAIN`) - - traefik.http.routers.backend.entrypoints=web,websecure - - traefik.http.routers.backend.tls=true - - traefik.http.routers.backend.tls.certresolver=defaultresolver - - traefik.http.routers.backend.service=backend - - traefik.http.services.backend.loadbalancer.server.port=8000 - - traefik.http.middlewares.backend.redirectscheme.scheme=https - - traefik.http.middlewares.backend.redirectscheme.permanent=true env_file: - $ENV depends_on: @@ -35,7 +25,10 @@ services: migrator: restart: on-failure:1 - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + container_name: migrator + build: + context: . + dockerfile: Dockerfile command: - /bin/sh - -c @@ -46,6 +39,10 @@ services: celery: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + container_name: celery command: celery -A backend worker -l INFO --concurrency 8 env_file: - $ENV @@ -57,47 +54,30 @@ services: celery_beat: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + container_name: beat command: celery -A backend beat -l INFO env_file: - $ENV depends_on: - celery-mdb - static-server: - image: $CI_REGISTRY_IMAGE/static-server:$CI_COMMIT_SHA - build: - context: nginx - dockerfile: Dockerfile - labels: - - traefik.enable=true - - traefik.docker.network=infrastructure - - traefik.http.routers.backend-static.rule=Host(`$DOMAIN`) && PathPrefix(`/static`) - - traefik.http.routers.backend-static.entrypoints=web,websecure - - traefik.http.routers.backend-static.tls=true - - traefik.http.routers.backend-static.tls.certresolver=defaultresolver - - traefik.http.routers.backend-static.service=backend-static - - traefik.http.services.backend-static.loadbalancer.server.port=80 - - traefik.http.middlewares.backend-static.redirectscheme.scheme=https - - traefik.http.middlewares.backend-static.redirectscheme.permanent=true - networks: - - infrastructure - volumes: - - static:/var/www/static - env_file: - - $ENV - - cache-mdb: image: redis:alpine restart: unless-stopped + container_name: cache-mdb celery-mdb: image: redis:alpine restart: unless-stopped + container_name: celery-mdb channels-mdb: image: redis:alpine restart: unless-stopped + container_name: channels-mdb networks: default: @@ -124,7 +124,7 @@ exclude = [ "migrations" ] -line-length = 109 +line-length = 90 indent-width = 4 target-version = "py312" @@ -3,13 +3,13 @@ services: app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest build: context: . dockerfile: Dockerfile tags: - - $CI_REGISTRY_IMAGE:latest - - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA cache_from: - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true cache_to: @@ -43,7 +43,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=infrastructure - - traefik.http.routers.backend.rule=Host(`$DOMAIN`) || Host(`backend.air.fail`) + - traefik.http.routers.backend.rule=Host(`backend.air.fail`) - traefik.http.routers.backend.entrypoints=web,websecure - traefik.http.routers.backend.tls=true - traefik.http.routers.backend.tls.certresolver=defaultresolver @@ -56,6 +56,16 @@ services: migrator: image: $CI_REGISTRY_IMAGE:latest + build: + context: . + dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true deploy: replicas: 1 restart_policy: @@ -71,7 +81,17 @@ services: - $ENV celery: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest + build: + context: . + dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true command: celery -A backend worker -l INFO --concurrency 8 networks: - default @@ -95,7 +115,17 @@ services: - C_FORCE_ROOT=true celery_beat: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest + build: + context: . + dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true command: celery -A backend beat -l INFO networks: - default @@ -129,7 +159,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=infrastructure - - traefik.http.routers.backend-static.rule=Host(`$DOMAIN`) && PathPrefix(`/static`) + - traefik.http.routers.backend-static.rule=Host(`backend.air.fail`) && PathPrefix(`/static`) - traefik.http.routers.backend-static.entrypoints=web,websecure - traefik.http.routers.backend-static.tls=true - traefik.http.routers.backend-static.tls.certresolver=defaultresolver