@@ -54,6 +54,13 @@ from payments.models.payment_plan import PaymentPlan from payments.selectors.payment_plan_selector import PaymentPlanSelector from payments.services.payment_plan_service import PaymentPlanService +from django.conf import settings +from rest_framework.exceptions import APIException +import logging +import httpx + + +logger = logging.getLogger(__name__) class BusinessHostService: def __init__(self, user: CustomUserModel): @@ -118,8 +125,23 @@ class BusinessHostService: serializer = NewBusinessAccountSerializer(data=request.data) serializer.is_valid(raise_exception=True) - account = self.create_existing(**serializer.validated_data).account - return BusinessAccountDataSerializer(account, context={'account_type': 'business_account'}) + email = serializer.validated_data['email'] + if CustomUserModel.objects.filter(email=serializer.validated_data.get("email")).exists(): + raise ValueError("Этот email уже зарегистрирован в системе.") + try: + response = httpx.get(settings.EMAIL_CHECKER_URL, params={'api_key': settings.EMAIL_CHECKER_API_KEY, 'email': email}) + if response.status_code == 422: + logger.error("Ошибка сервиса EMAILCHECKER.") + raise APIException(400, "не получилось отправить запрос, попробуйте позже") + elif response.status_code < 400 and response.json() and response.json().get('deliverability') == "DELIVERABLE": + account = self.create_existing(**serializer.validated_data).account + return BusinessAccountDataSerializer(account, context={'account_type': 'business_account'}) + else: + logger.error("Undeliverability email.") + raise Exception("Email недействительный") + except Exception as exc: + logger.exception("Ошибка при создании учетной записи:") + raise APIException(f"Произошла ошибка при создании учетной записи: {exc}") from exc def update_token_limit( self, @@ -16,6 +16,10 @@ from authentication.services.email_token_service import EmailTokenService from core.minio_service import MinIOService from reports.models.error_report import ErrorReport +from django.core.mail import send_mail +from django.utils.html import strip_tags + + logger = logging.getLogger(__name__) @@ -1,6 +1,7 @@ from typing import Any, Tuple from uuid import UUID +from django.conf import settings from django.contrib.auth import authenticate, login, logout from django.db.models import Q, QuerySet from django.db.transaction import atomic @@ -36,6 +37,7 @@ from authentication.serializers import ( ) from authentication.services.email_service import EmailService from authentication.services.utm_service import UTMService +from authentication.tasks import send_corporate_offer_task from authentication.utils import get_client_ip from core.minio_service import MinIOService from payments.services.referral_account import ReferralAccountService @@ -68,7 +70,6 @@ class UserService: ) user.save() - EmailService(user).send_reg_conf_email() if referer_username := user_data.get('referer'): try: @@ -80,6 +81,10 @@ class UserService: ReferralAccountService.create_invite(referer_account=referer.referral_account, invitee=user) except CustomUserModel.DoesNotExist: pass + + email_domain = user.email.split('@')[-1] + if email_domain not in settings.VALID_EMAIL_DOMAINS: + send_corporate_offer_task.apply_async(args=[user.email], countdown=5) return user def create_user_telegram(self, request: Request) -> TelegramUser: @@ -0,0 +1,21 @@ + + + + + + Корпоративный аккаунт AIR: работа с платформой для всей команды + + +

Здравствуйте!

+

В AIR можно оформить корпоративный аккаунт прямо на платформе, чтобы упростить работу для ваших коллег:

+ +

Оформить корпоративный аккаунт можно в настройках профиля. Если возникнут вопросы — мы всегда на связи!

+

Нужна помощь с оформлением доступа к платформе по ЭДО? Обращайтесь на info@air.fail

+

Всегда с вами,

+

Команда AIR

+ + @@ -0,0 +1,22 @@ +from celery import shared_task + +from django.template.loader import get_template + +from authentication.services.email_service import EmailService + +import logging + + +logger = logging.getLogger(__name__) + +@shared_task +def send_corporate_offer_task(email: str): + try: + template = get_template('corporate_offer_letter.html') + except Exception as exc: + logger.warning(f"Template error: {exc}, email: {email}") + return + html_message = template.render() + EmailService(None).send_email("Корпоративный аккаунт AIR", html_message, email) + + @@ -169,6 +169,7 @@ TEMPLATES = [ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ BASE_DIR / 'core' / 'templates', + BASE_DIR / 'authentication' / 'templates', ], 'APP_DIRS': True, 'OPTIONS': { @@ -327,6 +328,9 @@ FLUX_API_KEY = env.str('FLUX_API_KEY', 'defaultapikey') OPENROUTER_API_KEY = env.str('OPENROUTER_API_KEY', 'defaultapikey') FAL_API_KEY = env.str('FAL_API_KEY', 'defaultapikey') +EMAIL_CHECKER_API_KEY = env.str('EMAIL_CHECKER_API_KEY', 'defaultapikey') +EMAIL_CHECKER_URL = env.str('EMAIL_CHECKER_URL', 'https://emailvalidation.abstractapi.com/v1/') + OPENAI_PROXY_HOST = env.str('OPENAI_PROXY_HOST', 'neuron-proxy:8080') UPSCALE_MULTIPLIER_HOST = env.str('UPSCALE_MULTIPLIER_HOST', 'packet:8080') @@ -344,6 +348,7 @@ EMAIL_PORT = env.int('EMAIL_PORT', default=143) EMAIL_HOST_USER = env.str('EMAIL_HOST_USER', default='defaultuser') EMAIL_HOST_PASSWORD = env.str('EMAIL_HOST_PASSWORD', default='defaultpass') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +VALID_EMAIL_DOMAINS = env.str('VALID_EMAIL_DOMAINS', default='defaultdomain') 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') @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-10 12:12+0300\n" +"POT-Creation-Date: 2025-05-27 10:31+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,7 +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:32 +#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:34 #: stories/models.py:18 msgid "Icon" msgstr "Миниатюра" @@ -33,17 +33,17 @@ msgstr "Достижение" msgid "Achievements" msgstr "Достижения" -#: achievements/models.py:14 ml_model/models.py:25 ml_model/models.py:52 -#: ml_model/models.py:231 ml_model/models.py:357 +#: achievements/models.py:14 ml_model/models.py:27 ml_model/models.py:47 +#: ml_model/models.py:79 ml_model/models.py:289 ml_model/models.py:434 msgid "Slug" msgstr "Ярлык" -#: achievements/models.py:16 ml_model/models.py:51 ml_model/models.py:146 -#: ml_model/models.py:230 ml_model/models.py:355 payments/models/payment.py:52 +#: achievements/models.py:16 ml_model/models.py:78 ml_model/models.py:186 +#: ml_model/models.py:288 ml_model/models.py:432 payments/models/payment.py:52 msgid "Description" msgstr "Описание" -#: achievements/models.py:43 authentication/models/business_host.py:21 +#: achievements/models.py:43 authentication/models/business_host.py:22 #: authentication/models/email_token.py:12 authentication/models/user.py:247 #: authentication/models/user.py:248 authentication/models/user_telegram.py:22 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 @@ -69,8 +69,8 @@ msgid "Business account not found" msgstr "Сотрудник не найден" #: authentication/exceptions/business_host_exceptions/access_denied.py:6 -#: authentication/services/business_account_service.py:103 -#: authentication/services/business_account_service.py:106 +#: authentication/services/business_account_service.py:105 +#: authentication/services/business_account_service.py:108 msgid "You do not have sufficient rights to perform this action" msgstr "У вас недостаточно прав для выполнения этого действия" @@ -149,9 +149,11 @@ msgid "Group" msgstr "Группа" #: authentication/models/business_account.py:61 -msgid "Impossible to add this employee to this group which does not belong to this " +msgid "" +"Impossible to add this employee to this group which does not belong to this " "company" -msgstr "Невозможно добавить сотрудника к группе, когда он не принадлежит данной " +msgstr "" +"Невозможно добавить сотрудника к группе, когда он не принадлежит данной " "компании" #: authentication/models/business_account.py:70 @@ -162,8 +164,8 @@ msgstr "Дочерний Бизнес Аккаунт" msgid "Child Business Accounts" msgstr "Дочерние Бизнес Аккаунты" -#: authentication/models/business_group.py:8 ml_model/models.py:24 -#: ml_model/models.py:145 ml_model/models.py:348 +#: authentication/models/business_group.py:8 ml_model/models.py:26 +#: ml_model/models.py:185 ml_model/models.py:425 #: payments/models/payment_plan.py:27 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" @@ -177,81 +179,85 @@ msgstr "Бизнес Группа" msgid "Business Groups" msgstr "Бизнес Группы" -#: authentication/models/business_host.py:30 +#: authentication/models/business_host.py:31 msgid "Affiliated by" msgstr "Кем привлечена" -#: authentication/models/business_host.py:35 authentication/models/user.py:153 +#: authentication/models/business_host.py:36 authentication/models/user.py:153 #: authentication/models/whitelist.py:16 payments/models/promocode.py:85 msgid "Is active" msgstr "Является активной" -#: authentication/models/business_host.py:42 +#: authentication/models/business_host.py:43 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:50 msgid "Usage intensity" msgstr "Частота использования" -#: authentication/models/business_host.py:56 +#: authentication/models/business_host.py:57 msgid "Token low balance cap" msgstr "Предел низкого баланса" -#: authentication/models/business_host.py:61 +#: authentication/models/business_host.py:62 msgid "Emails token low balance cap" msgstr "Email'ы для рассылки по низкому балансу" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:64 msgid "Token low balance cap enabled" msgstr "Рассылка по низкому балансу включена" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:67 msgid "ITN" msgstr "ИНН" -#: authentication/models/business_host.py:67 +#: authentication/models/business_host.py:68 msgid "PSRN" msgstr "ОГРН" -#: authentication/models/business_host.py:68 ml_model/models.py:50 -#: ml_model/models.py:228 tools/public_api/models.py:30 +#: authentication/models/business_host.py:69 ml_model/models.py:46 +#: ml_model/models.py:77 ml_model/models.py:286 tools/public_api/models.py:30 msgid "Name" msgstr "Наименование" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:72 msgid "Preffered name" msgstr "Предпочтительное имя" -#: authentication/models/business_host.py:72 +#: authentication/models/business_host.py:73 msgid "Corporate email" msgstr "Корпоративная почта" -#: authentication/models/business_host.py:74 +#: authentication/models/business_host.py:75 msgid "Corporate phone" msgstr "Корпоративный телефон" -#: authentication/models/business_host.py:76 +#: authentication/models/business_host.py:77 msgid "Job title" msgstr "Наименование работ" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:83 msgid "Allowed models" msgstr "Разрешенные модели" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:87 +msgid "Private models" +msgstr "Приватные модели" + +#: authentication/models/business_host.py:90 msgid "Log history enabled" msgstr "История логов включена" -#: authentication/models/business_host.py:103 +#: authentication/models/business_host.py:109 msgid "Business Account" msgstr "Бизнес Аккаунт" -#: authentication/models/business_host.py:104 +#: authentication/models/business_host.py:110 msgid "Business Accounts" msgstr "Бизнес Аккаунты" @@ -267,10 +273,6 @@ msgstr "IT" msgid "Finance" msgstr "Финансы" -#: authentication/models/business_host.py:86 -msgid "Private models" -msgstr "Приватные модели" - #: authentication/models/choices.py:9 msgid "Tourism" msgstr "Туризм" @@ -323,7 +325,7 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:15 ml_model/models.py:147 +#: authentication/models/email_token.py:15 ml_model/models.py:187 msgid "Key" msgstr "Ключ" @@ -400,7 +402,7 @@ msgid "Phonenumber" msgstr "Номер телефона" #: authentication/models/user_telegram.py:27 -#: authentication/models/user_vk.py:14 ml_model/models.py:318 +#: authentication/models/user_vk.py:14 ml_model/models.py:392 #: payments/models/invoice.py:11 stories/models.py:15 tools/chats/models.py:10 msgid "Created at" msgstr "Когда создан" @@ -458,12 +460,13 @@ msgstr "Вайтлист для отмены политик" msgid "Whitelists to cancel policies" msgstr "Вайтлисты для отмены политик" -#: authentication/selectors/business_host_selector.py:40 -#: authentication/selectors/business_host_selector.py:83 +#: authentication/selectors/business_host_selector.py:38 +#: authentication/selectors/business_host_selector.py:81 msgid "User haven't rights to access host account information" -msgstr "У пользователя недостаточно прав для просмотра информации бизнес-аккаунта" +msgstr "" +"У пользователя недостаточно прав для просмотра информации бизнес-аккаунта" -#: authentication/selectors/business_host_selector.py:58 +#: authentication/selectors/business_host_selector.py:56 msgid "Host user is not registered for this account" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" @@ -479,53 +482,57 @@ msgstr "Бизнес-аккаунт для данного юзера не най msgid "Invited account can either accept or reject an invitation" msgstr "Приглашенный аккаунт может принять или отклонить приглашение" -#: authentication/services/business_account_service.py:81 +#: authentication/services/business_account_service.py:83 msgid "Account is already confirmed" msgstr "Аккаунт уже подтвержден" -#: authentication/services/business_account_service.py:100 +#: authentication/services/business_account_service.py:102 #, fuzzy #| msgid "Passwords do not match" msgid "Passwords don't match" msgstr "Пароли не совпадают" -#: authentication/services/business_host_service.py:151 +#: authentication/services/business_account_service.py:111 +msgid "You cannot change the password of an unconfirmed e-mail user." +msgstr "Вы не можете изменить пароль неподтвержденного по e-mail пользователя." + +#: authentication/services/business_host_service.py:171 msgid "No user_email is provided" msgstr "" -#: authentication/services/email_service.py:47 +#: authentication/services/email_service.py:51 msgid "Error occured when proceed email sending" msgstr "Случилась ошибка во время отправки email" -#: authentication/services/email_service.py:124 +#: authentication/services/email_service.py:128 msgid "Regular users cannot send introductory letters" msgstr "Обычные пользователи не могут отсылать письма" -#: authentication/services/email_service.py:158 +#: authentication/services/email_service.py:163 msgid "Regular users cannot send invitation letters" msgstr "Обычные пользователи не могут отправлять письма для приглашений" -#: authentication/services/user_services.py:162 +#: authentication/services/user_services.py:167 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:179 +#: authentication/services/user_services.py:184 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:203 +#: authentication/services/user_services.py:208 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:207 +#: authentication/services/user_services.py:212 msgid "No token like this in a database" msgstr "Не найдено такого токена" -#: authentication/services/user_services.py:213 +#: authentication/services/user_services.py:218 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:251 +#: authentication/services/user_services.py:256 msgid "Current password is wrong" msgstr "Текущий пароль неверен" @@ -572,16 +579,16 @@ msgstr "Неизвестный бакет для загрузки" msgid "The file size cannot exceed %(max_mb_size)d MB" msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" -#: ml_model/admin.py:74 ml_model/models.py:263 ml_model/models.py:269 -#: ml_model/models.py:301 ml_model/models.py:323 +#: ml_model/admin.py:114 ml_model/models.py:335 ml_model/models.py:341 +#: ml_model/models.py:375 ml_model/models.py:397 msgid "Inference" msgstr "Инференс" -#: ml_model/admin.py:75 ml_model/models.py:264 ml_model/models.py:377 +#: ml_model/admin.py:115 ml_model/models.py:336 ml_model/models.py:459 msgid "Inferences" msgstr "Инференсы" -#: ml_model/apps.py:8 ml_model/models.py:388 +#: ml_model/apps.py:8 ml_model/models.py:482 msgid "Neuron Models" msgstr "Нейронные Модели" @@ -594,351 +601,383 @@ msgstr "Инференс в настоящее время выключен, по msgid "Parameter %(parameter_name)s not valid, please retry later" msgstr "Параметр %(parameter_name)s некорректен, повторите попытку позже" -#: ml_model/models.py:29 +#: ml_model/exceptions.py:26 +#, fuzzy +#| msgid "Payment Rule" +msgid "Payment Rule not implemented" +msgstr "Платежное правило" + +#: ml_model/exceptions.py:31 +msgid "Scraper does not exists" +msgstr "" + +#: ml_model/exceptions.py:39 +#, python-format +msgid "Format is not supported. Supported formats: %(formats)s" +msgstr "Формат не поддерживается. Поддерживаемые форматы: %(formats)s" + +#: ml_model/models.py:31 msgid "Not SVG-pictures not allowed" msgstr "Нельзя использовать не SVG-картинки" -#: ml_model/models.py:39 +#: ml_model/models.py:41 #, fuzzy #| msgid "Tags" msgid "Tag" msgstr "Теги" -#: ml_model/models.py:40 ml_model/models.py:239 +#: ml_model/models.py:42 ml_model/models.py:297 msgid "Tags" msgstr "Теги" -#: ml_model/models.py:45 ml_model/models.py:99 -#: reports/models/error_report.py:10 +#: ml_model/models.py:57 ml_model/models.py:86 +msgid "Scraper" +msgstr "" + +#: ml_model/models.py:60 +msgid "Keyword Arguments" +msgstr "" + +#: ml_model/models.py:67 +#, fuzzy +#| msgid "Runner is missing" +msgid "Scraper is missing" +msgstr "Раннер не найден" + +#: ml_model/models.py:72 ml_model/models.py:139 ml_model/models.py:234 +#: ml_model/models.py:420 reports/models/error_report.py:10 msgid "Text" msgstr "Текст" -#: ml_model/models.py:46 +#: ml_model/models.py:73 ml_model/models.py:235 msgid "File" msgstr "Файл" -#: ml_model/models.py:47 +#: ml_model/models.py:74 ml_model/models.py:236 msgid "Embeddings" msgstr "Эмбеддинги" -#: ml_model/models.py:49 ml_model/models.py:227 +#: ml_model/models.py:76 ml_model/models.py:285 msgid "ID" msgstr "ID" -#: ml_model/exceptions.py:20 -msgid "The model is not responding" -msgstr "Модель не отвечает" - -#: ml_model/models.py:63 +#: ml_model/models.py:99 msgid "Runner" msgstr "Раннер" -#: ml_model/models.py:66 +#: ml_model/models.py:102 msgid "Output Type" msgstr "Тип исходящего контента" -#: ml_model/models.py:68 ml_model/models.py:241 +#: ml_model/models.py:104 ml_model/models.py:299 msgid "Enabled" msgstr "Включен" -#: ml_model/models.py:75 +#: ml_model/models.py:111 msgid "Runner is missing" msgstr "Раннер не найден" -#: ml_model/models.py:93 ml_model/models.py:119 ml_model/models.py:162 -#: ml_model/models.py:210 ml_model/models.py:236 +#: ml_model/models.py:133 ml_model/models.py:159 ml_model/models.py:202 +#: ml_model/models.py:268 ml_model/models.py:294 msgid "Deployment" msgstr "Деплоймент" -#: ml_model/models.py:94 +#: ml_model/models.py:134 msgid "Deployments" msgstr "Деплойменты" -#: ml_model/models.py:100 ml_model/models.py:346 stories/models.py:36 +#: ml_model/models.py:140 ml_model/models.py:421 stories/models.py:36 msgid "Image" msgstr "Картинка" -#: ml_model/models.py:101 +#: ml_model/models.py:141 msgid "PDF" msgstr "PDF" -#: ml_model/models.py:102 +#: ml_model/models.py:142 msgid "DOCX" msgstr "DOCX" -#: ml_model/models.py:103 +#: ml_model/models.py:143 msgid "DOC" msgstr "DOC" -#: ml_model/models.py:104 +#: ml_model/models.py:144 msgid "Text File (Notebook)" msgstr "Текстовый файл (Блокнот)" -#: ml_model/models.py:105 +#: ml_model/models.py:145 msgid "ZIP Archive" msgstr "ZIP архив" -#: ml_model/models.py:106 +#: ml_model/models.py:146 msgid "Audio" msgstr "Аудио" -#: ml_model/models.py:112 ml_model/models.py:148 ml_model/models.py:296 -#: ml_model/models.py:371 payments/models/promocode.py:41 +#: ml_model/models.py:152 ml_model/models.py:188 ml_model/models.py:370 +#: ml_model/models.py:449 payments/models/promocode.py:41 msgid "Type" msgstr "Тип" -#: ml_model/models.py:114 ml_model/models.py:157 ml_model/models.py:276 +#: ml_model/models.py:154 ml_model/models.py:197 ml_model/models.py:348 msgid "Required" msgstr "Обязательный" -#: ml_model/models.py:124 +#: ml_model/models.py:164 #, python-format msgid "%(input_type)s input of %(deployment_title)s" msgstr "Входящий поток типа %(input_type)s деплоймента %(deployment_title)s" -#: ml_model/models.py:130 +#: ml_model/models.py:170 #, fuzzy #| msgid "Model Input" msgid "Input" msgstr "Модель" -#: ml_model/models.py:131 +#: ml_model/models.py:171 #, fuzzy #| msgid "Model Inputs" msgid "Inputs" msgstr "Входящий поток модели" -#: ml_model/models.py:137 +#: ml_model/models.py:177 msgid "Integer" msgstr "Целое число" -#: ml_model/models.py:138 +#: ml_model/models.py:178 msgid "Float" msgstr "Вещественное число" -#: ml_model/models.py:139 +#: ml_model/models.py:179 msgid "String" msgstr "Строка" -#: ml_model/models.py:140 +#: ml_model/models.py:180 #, fuzzy #| msgid "Invoices" msgid "Choices" msgstr "Списания" -#: ml_model/models.py:141 +#: ml_model/models.py:181 msgid "Float range" msgstr "Вещественный диапазон" -#: ml_model/models.py:142 +#: ml_model/models.py:182 msgid "Integer range" msgstr "Целочисленный диапазон" -#: ml_model/models.py:143 +#: ml_model/models.py:183 msgid "Logical" msgstr "Логический" -#: ml_model/models.py:153 +#: ml_model/models.py:193 msgid "Values" msgstr "Значения" -#: ml_model/models.py:154 -msgid "These values can contain different interfaces and default value optional" -msgstr "Значения могут содержать различные интерфейс и, опционально, значение по " +#: ml_model/models.py:194 +msgid "" +"These values can contain different interfaces and default value optional" +msgstr "" +"Значения могут содержать различные интерфейс и, опционально, значение по " "умолчанию" -#: ml_model/models.py:156 ml_model/models.py:275 +#: ml_model/models.py:196 ml_model/models.py:347 msgid "Hidden" msgstr "Скрытый" -#: ml_model/models.py:167 +#: ml_model/models.py:208 +msgid "Key \"default\" is required" +msgstr "" + +#: ml_model/models.py:211 #, fuzzy, python-format #| msgid "Parameter of %(model_title)s" msgid "Parameter \"%(key)s\" of %(deployment_title)s" msgstr "Параметр %(model_title)s" -#: ml_model/models.py:173 ml_model/models.py:272 +#: ml_model/models.py:217 ml_model/models.py:344 msgid "Parameter" msgstr "Параметр" -#: ml_model/models.py:174 +#: ml_model/models.py:218 msgid "Parameters" msgstr "Параметры" -#: ml_model/models.py:180 +#: ml_model/models.py:224 msgid "Fixed" msgstr "Фикса" -#: ml_model/models.py:181 +#: ml_model/models.py:225 msgid "Per generation second" msgstr "За секунду генерации" -#: ml_model/models.py:182 +#: ml_model/models.py:226 msgid "Per one text token" msgstr "За один текстовый токен" -#: ml_model/models.py:183 +#: ml_model/models.py:227 msgid "Per image pixel" msgstr "За один пиксель" -#: ml_model/models.py:186 +#: ml_model/models.py:230 msgid "By input data" msgstr "По входящим данным" -#: ml_model/models.py:187 +#: ml_model/models.py:231 msgid "By output data" msgstr "По исходящим данным" -#: ml_model/models.py:188 -msgid "By all data" -msgstr "По всем данным" - -#: ml_model/models.py:193 +#: ml_model/models.py:241 msgid "Strategy" msgstr "Стратегия" -#: ml_model/models.py:198 +#: ml_model/models.py:248 msgid "Interaction Type" msgstr "Тип взаимодействия" -#: ml_model/models.py:203 payments/models/invoice.py:19 +#: ml_model/models.py:255 +#, fuzzy +#| msgid "Interaction Type" +msgid "Content Type" +msgstr "Тип взаимодействия" + +#: ml_model/models.py:261 payments/models/invoice.py:19 msgid "Cost" msgstr "Цена" -#: ml_model/models.py:204 +#: ml_model/models.py:262 msgid "In RUB, per specified strategy" msgstr "В рублях, за указанную стратегию" -#: ml_model/models.py:215 +#: ml_model/models.py:273 #, python-format -msgid "Payment Rule \"%(strategy)s\"/\"%(interaction_type)s\" of " +msgid "" +"Payment Rule \"%(strategy)s\"/\"%(interaction_type)s\" of " +"%(deployment_title)s" +msgstr "" +"Платежное правило \"%(strategy)s\"/\"%(interaction_type)s\" деплоймента " "%(deployment_title)s" -msgstr "Платежное правило \"%(strategy)s\"/\"%(interaction_type)s\" деплоймента %(deployment_title)s" -#: ml_model/models.py:222 +#: ml_model/models.py:280 msgid "Payment Rule" msgstr "Платежное правило" -#: ml_model/models.py:223 +#: ml_model/models.py:281 msgid "Payment Rules" msgstr "Платежные правила" -#: ml_model/models.py:245 +#: ml_model/models.py:303 msgid "Inference cannot be available when parent Deployment is disabled" -msgstr "Инференс не может быть доступен, когда родительский Деплоймент выключен" +msgstr "" +"Инференс не может быть доступен, когда родительский Деплоймент выключен" -#: ml_model/models.py:274 +#: ml_model/models.py:346 msgid "Value" msgstr "Значение" -#: ml_model/models.py:280 +#: ml_model/models.py:354 msgid "Parameter must be hidden cause parent is hidden" msgstr "Параметр должен быть скрыт, потому что родительский также скрыт" -#: ml_model/models.py:282 +#: ml_model/models.py:356 msgid "Parameter must be required cause parent is required" -msgstr "Параметр должен быть обязательным, потому что родительский также обязателен" +msgstr "" +"Параметр должен быть обязательным, потому что родительский также обязателен" -#: ml_model/models.py:285 +#: ml_model/models.py:359 msgid "Overriden Parameter" msgstr "Переопределенный параметр" -#: ml_model/models.py:286 +#: ml_model/models.py:360 msgid "Overriden Parameters" msgstr "Переопределенные параметры" -#: ml_model/models.py:292 +#: ml_model/models.py:366 msgid "Addition" msgstr "Сложение" -#: ml_model/models.py:293 +#: ml_model/models.py:367 msgid "Multiplication" msgstr "Умножение" -#: ml_model/models.py:295 +#: ml_model/models.py:369 msgid "Coefficient" msgstr "Коэффициент" -#: ml_model/models.py:308 +#: ml_model/models.py:382 #, python-format msgid "Payment Bias of %(inference_title)s" msgstr "Платежный сдвиг %(inference_title)s" -#: ml_model/models.py:311 ml_model/models.py:312 +#: ml_model/models.py:385 ml_model/models.py:386 msgid "Payment Bias" msgstr "Платежные сдвиг" -#: ml_model/models.py:316 +#: ml_model/models.py:390 msgid "Generation time" msgstr "Время генерации" -#: ml_model/models.py:317 +#: ml_model/models.py:391 msgid "Tokens cost" msgstr "Стоимость в токенах" -#: ml_model/models.py:328 +#: ml_model/models.py:402 #, python-format msgid "Tracking Record created at %(created_at)s of %(inference_title)s" msgstr "" -#: ml_model/models.py:334 +#: ml_model/models.py:408 msgid "Tracking Record" msgstr "Отслеживающая запись" -#: ml_model/models.py:335 +#: ml_model/models.py:409 msgid "Tracking Records" msgstr "Отслеживающие записи" -#: ml_model/models.py:345 +#: ml_model/models.py:419 msgid "Chat-bots" msgstr "Чат-боты" -#: ml_model/models.py:353 +#: ml_model/models.py:422 +msgid "Video" +msgstr "" + +#: ml_model/models.py:423 +msgid "Code" +msgstr "" + +#: ml_model/models.py:430 msgid "Alternative Titles" msgstr "Альтернативные названия" -#: ml_model/models.py:359 +#: ml_model/models.py:436 msgid "Fill automatically, don't touch" msgstr "Заполняется автоматически, не трогать" -#: ml_model/models.py:368 +#: ml_model/models.py:445 msgid "Avatar" msgstr "Аватар" -#: ml_model/models.py:387 +#: ml_model/models.py:452 +#, fuzzy +#| msgid "Type" +msgid "Types" +msgstr "Тип" + +#: ml_model/models.py:481 msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/selectors/ml_models_selector.py:57 -msgid "no model by this id" -msgstr "Не найдено моделей по этому ID" - -#: ml_model/services/chatgpt.py:125 -msgid "Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)" -msgstr "Невозможно распознать изображение. (Поддерживаемые форматы: PNG, JPG, JPEG)" - -#: ml_model/services/chatgpt.py:150 -msgid "No matching version found" -msgstr "Соответствующая версия не найдена" - -#: ml_model/services/inference.py:49 +#: ml_model/services/inference.py:101 msgid "Payment rules are missing; Inference: {}" msgstr "" -#: ml_model/services/inference.py:134 +#: ml_model/services/inference.py:212 #, fuzzy #| msgid "No matching version found" msgid "No tracking records found" msgstr "Соответствующая версия не найдена" -#: ml_model/services/inference.py:171 -msgid "Unable to predict price" -msgstr "" - -#: ml_model/services/upscaleai.py:80 -msgid "No image given for improving" -msgstr "Нет изображения для улучшения" - #: payments/apps.py:9 payments/models/payment.py:60 msgid "Payments" msgstr "Платежи" @@ -1080,10 +1119,6 @@ msgstr "Платежный метод" msgid "Payment Methods" msgstr "Платежные методы" -#: payments/selectors/model_payment_selector.py:25 -msgid "Messages for this model are not registered in a selector" -msgstr "" - #: payments/selectors/payment_plan_selector.py:32 msgid "Business accounts are not allowed to make purchases" msgstr "Сотрудники не могут производить покупки" @@ -1196,6 +1231,15 @@ msgstr "Медиа" msgid "Feed" msgstr "Шейр пользователей" +#: tools/chats/apis.py:144 tools/chats/consumers.py:48 tools/media/apis.py:143 +#: tools/public_api/views/base.py:69 +msgid "Enabled inference not found in model" +msgstr "" + +#: tools/chats/consumers.py:25 +msgid "User haven't permissions to connect to orders queue" +msgstr "" + #: tools/chats/models.py:13 tools/public_api/models.py:45 msgid "Is deleted" msgstr "Удален" @@ -1209,6 +1253,10 @@ msgstr "Чат %(id)s" msgid "Chat" msgstr "Чат" +#: tools/chats/services/chat.py:21 +msgid "New chat" +msgstr "" + #: tools/public_api/exceptions.py:7 msgid "Upgrade token limit on your api-key" msgstr "Необходимо повысить лимит токенов у API-ключа" @@ -1229,23 +1277,46 @@ msgstr "API Ключ" msgid "API Keys" msgstr "API Ключи" -#: tools/public_api/views/base.py:58 +#: tools/public_api/views/base.py:49 msgid "Key limit exceeded" msgstr "Превышен лимит по ключу" -#: tools/public_api/views/base.py:63 +#: tools/public_api/views/base.py:55 msgid "Model is blocked by outdating or temporary block, please retry later" msgstr "" "Модель заблокирована, т.к закончила обновляться или временно заблокирована, " "попробуйте позже" -#: tools/public_api/views/base.py:90 -msgid "" -"Error occured when create generation. It may cause NSFW-content not allowed, " -"retry again" -msgstr "" -"Случилась ошибка во время генерации. Она может возникать из-за того, что " -"NSFW-контент запрещен. Попробуйте снова" +#~ msgid "No business account by this uid at your company" +#~ msgstr "Такого аккаунта нет в вашей компании" + +#~ msgid "The model is not responding" +#~ msgstr "Модель не отвечает" + +#~ msgid "By all data" +#~ msgstr "По всем данным" + +#~ msgid "no model by this id" +#~ msgstr "Не найдено моделей по этому ID" + +#~ msgid "" +#~ "Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)" +#~ msgstr "" +#~ "Невозможно распознать изображение. (Поддерживаемые форматы: PNG, JPG, " +#~ "JPEG)" + +#~ msgid "No matching version found" +#~ msgstr "Соответствующая версия не найдена" + +#~ msgid "No image given for improving" +#~ msgstr "Нет изображения для улучшения" + +#~ msgid "" +#~ "Error occured when create generation. It may cause NSFW-content not " +#~ "allowed, retry again" +#~ msgstr "" +#~ "Случилась ошибка во время генерации. Она может возникать из-за того, что " +#~ "NSFW-контент запрещен. Попробуйте снова" #~ msgid "Category" #~ msgstr "Категория" @@ -1298,6 +1369,9 @@ msgstr "" #~ msgid "Rate" #~ msgstr "Ставка" - -msgid "You cannot change the password of an unconfirmed e-mail user." -msgstr "Вы не можете изменить пароль неподтвержденного по e-mail пользователя." +#~ msgid "" +#~ "Model version not found or does not exist. Available versions: " +#~ "%(available_versions)s" +#~ msgstr "" +#~ "Версия модели не найдена или не существует. Доступные версии: " +#~ "%(available_versions)s" @@ -1,6 +1,5 @@ # Generated by Django 5.0.11 on 2025-05-04 11:29 -from copy import deepcopy import django.db.models.deletion from django.db import migrations, models @@ -48,30 +47,36 @@ def migrate_fks_versions_to_deployments(apps, schema_editor): ModelInput = apps.get_model('ml_model', 'ModelInput') Deployment = apps.get_model('ml_model', 'Deployment') - old_inputs = ModelInput.objects.all() inputs = [] - ModelInput.objects.all().delete() - old_parameters = ModelParameter.objects.all() parameters = [] - ModelParameter.objects.all().delete() - for input in old_inputs: - for version in input.versions: - new_input = deepcopy(input) - new_input.id = None - new_input.inference = Deployment.objects.get(slug=version.slug) - inputs.append(new_input) + for input in ModelInput.objects.all(): + for version in input.versions.all(): + inputs.append(ModelInput(deployment=Deployment.objects.get(slug=version.slug), type=input.type, model=input.model, required=input.required)) + + for parameter in ModelParameter.objects.all(): + for version in parameter.versions.all(): + parameters.append( + ModelParameter( + deployment=Deployment.objects.get(slug=version.slug), + name=parameter.name, + model=parameter.model, + description=parameter.description, + order=parameter.order, + key=parameter.key, + type=parameter.type, + values=parameter.values, + hidden=parameter.hidden, + required=parameter.required + ) + ) - for parameter in old_parameters: - for version in parameter.versions: - new_parameter = deepcopy(parameter) - new_parameter.id = None - new_parameter.inference = Deployment.objects.get(slug=version.slug) - parameters.append(new_parameter) + ModelParameter.objects.all().delete() + ModelInput.objects.all().delete() - ModelInput.objects.bulk_create(inputs) - ModelParameter.objects.bulk_create(parameters) + ModelInput.objects.bulk_create(inputs, ignore_conflicts=True) + ModelParameter.objects.bulk_create(parameters, ignore_conflicts=True) class Migration(migrations.Migration): @@ -88,7 +93,8 @@ class Migration(migrations.Migration): ), migrations.RunPython( code=migrate_nm_type, - reverse_code=migrations.RunPython.noop + reverse_code=migrations.RunPython.noop, + atomic=True ), migrations.RemoveField( model_name='neuronmodel', @@ -128,187 +134,4 @@ class Migration(migrations.Migration): code=migrate_fks_versions_to_deployments, reverse_code=migrations.RunPython.noop ), - migrations.AlterField( - model_name='modelinput', - name='deployment', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='deployment_inputs', to='ml_model.deployment', verbose_name='Deployment'), - ), - migrations.AlterField( - model_name='modelparameter', - name='deployment', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='deployment_parameters', to='ml_model.deployment', verbose_name='Deployment'), - ), - migrations.AlterField( - model_name='modelpaymentrule', - name='inference', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_%(class)s', to='ml_model.inference', verbose_name='Inference'), - ), - migrations.AlterField( - model_name='modelstat', - name='inference', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_tracking_records', to='ml_model.inference', verbose_name='Inference'), - ), - migrations.DeleteModel( - name='ModelCategory', - ), - migrations.RemoveField( - model_name='modelinput', - name='model', - ), - migrations.RemoveField( - model_name='modelinput', - name='versions', - ), - migrations.RemoveField( - model_name='modelparameter', - name='model', - ), - migrations.RemoveField( - model_name='modelparameter', - name='versions', - ), - migrations.RemoveField( - model_name='modelpaymentrule', - name='model', - ), - migrations.RemoveField( - model_name='modelpaymentrule', - name='versions', - ), - migrations.RemoveField( - model_name='modelstat', - name='model', - ), - migrations.RemoveField( - model_name='neuronmodel', - name='tags', - ), - migrations.RemoveField( - model_name='modelversion', - name='model', - ), - migrations.DeleteModel( - name='ModelVersion', - ), - migrations.RenameModel( - old_name='ModelTag', - new_name='Tag', - ), - migrations.RenameModel( - old_name='ModelInput', - new_name='Input', - ), - migrations.RenameModel( - old_name='ModelParameter', - new_name='Parameter', - ), - migrations.RenameModel( - old_name='ModelPaymentRule', - new_name='PaymentRule', - ), - migrations.RenameModel( - old_name='ModelStat', - new_name='TrackingRecord', - ), - migrations.CreateModel( - name='PaymentBias', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('coefficient', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Coefficient')), - ('type', models.CharField(choices=[('addition', 'Addition'), ('multiplication', 'Multiplication')], max_length=32, verbose_name='Type')), - ('inference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_payment_biases', to='ml_model.inference', verbose_name='Inference')), - ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), - ], - options={ - 'ordering': ('order',), - 'verbose_name': 'Payment Bias', - 'verbose_name_plural': 'Payment Bias', - }, - ), - migrations.DeleteModel( - name='PaymentRule', - ), - migrations.AlterModelOptions( - name='input', - options={'verbose_name': 'Input', 'verbose_name_plural': 'Inputs'}, - ), - migrations.AlterModelOptions( - name='parameter', - options={'verbose_name': 'Parameter', 'verbose_name_plural': 'Parameters'}, - ), - migrations.AlterModelOptions( - name='tag', - options={'verbose_name': 'Tag', 'verbose_name_plural': 'Tags'}, - ), - migrations.AlterModelOptions( - name='trackingrecord', - options={'ordering': ('-created_at',), 'verbose_name': 'Tracking Record', 'verbose_name_plural': 'Tracking Records'}, - ), - migrations.RemoveField( - model_name='parameter', - name='order', - ), - migrations.AddField( - model_name='inference', - name='tags', - field=models.ManyToManyField(blank=True, related_name='inferences_tags', to='ml_model.tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='inference', - name='name', - field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Name'), - ), - migrations.AlterField( - model_name='parameter', - name='type', - field=models.CharField(choices=[('int', 'Integer'), ('float', 'Float'), ('str', 'String'), ('choices', 'Choices'), ('floatrange', 'Float range'), ('intrange', 'Integer range'), ('bool', 'Logical')], max_length=40, verbose_name='Type'), - ), - migrations.AlterField( - model_name='trackingrecord', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='trackingrecord', - name='generation_time', - field=models.DurationField(verbose_name='Generation time'), - ), - migrations.AlterField( - model_name='trackingrecord', - name='tokens_cost', - field=models.DecimalField(decimal_places=10, max_digits=50, verbose_name='Tokens cost'), - ), - migrations.CreateModel( - name='OverridenParameter', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.JSONField(verbose_name='Value')), - ('hidden', models.BooleanField(default=False, verbose_name='Hidden')), - ('required', models.BooleanField(default=False, verbose_name='Required')), - ('inference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_parameters', to='ml_model.inference', verbose_name='Inference')), - ('parameter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters_overriden', to='ml_model.parameter', verbose_name='Parameter')), - ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), - ], - options={ - 'ordering': ('order',), - 'verbose_name': 'Overriden Parameter', - 'verbose_name_plural': 'Overriden Parameters', - 'unique_together': {('parameter', 'inference')}, - }, - ), - migrations.CreateModel( - name='PaymentRule', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('strategy', models.CharField(choices=[('fixed', 'Fixed'), ('per-second', 'Per generation second'), ('per-text-token', 'Per one text token'), ('per-pixel', 'Per image pixel')], max_length=32, verbose_name='Strategy')), - ('cost', models.DecimalField(decimal_places=8, help_text='In RUB, per specified strategy', max_digits=10, verbose_name='Cost')), - ('deployment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='deployment_payment_rules', to='ml_model.deployment', verbose_name='Deployment')), - ('content_type', models.CharField(blank=True, choices=[('text', 'Text'), ('file', 'File'), ('embeddings', 'Embeddings')], max_length=32, null=True, verbose_name='Content Type')), - ('interaction_type', models.CharField(blank=True, choices=[('input', 'By input data'), ('output', 'By output data')], max_length=32, null=True, verbose_name='Interaction Type')), - ], - options={ - 'verbose_name': 'Payment Rule', - 'verbose_name_plural': 'Payment Rules', - }, - ), ] @@ -1,8 +1,15 @@ # Generated by Django 5.0.11 on 2025-05-27 07:29 +import django from django.db import migrations, models +def cleanup_payment_rules(apps, schema_editor): + ModelPaymentRule = apps.get_model('ml_model', 'ModelPaymentRule') + ModelPaymentRule.objects.all().delete() +def cleanup_tracking_records(apps, schema_editor): + ModelStat = apps.get_model('ml_model', 'ModelStat') + ModelStat.objects.all().delete() class Migration(migrations.Migration): dependencies = [ @@ -10,9 +17,12 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='deployment', - name='runner_import_path', - field=models.CharField(choices=[('ml_model.runners:FalAIRunner', 'FalAI'), ('ml_model.runners:OpenAIGPTRunner', 'OpenAIGPT'), ('ml_model.runners:OpenrouterRunner', 'Openrouter'), ('ml_model.runners:ReplicateAudioRunner', 'ReplicateAudio'), ('ml_model.runners:ReplicateImageRunner', 'ReplicateImage'), ('ml_model.runners:ReplicateTextRunner', 'ReplicateText'), ('ml_model.runners:ReplicateVideoRunner', 'ReplicateVideo')], max_length=100, verbose_name='Runner'), + migrations.RunPython( + code=cleanup_tracking_records, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=cleanup_payment_rules, + reverse_code=migrations.RunPython.noop ), ] @@ -0,0 +1,201 @@ +# Generated by Django 5.0.11 on 2025-05-27 21:42 + +import django +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0056_alter_deployment_runner_import_path'), + ] + + operations = [ + migrations.AlterField( + model_name='deployment', + name='runner_import_path', + field=models.CharField(choices=[('ml_model.runners:FalAIRunner', 'FalAI'), ('ml_model.runners:OpenAIGPTRunner', 'OpenAIGPT'), ('ml_model.runners:OpenrouterRunner', 'Openrouter'), ('ml_model.runners:ReplicateAudioRunner', 'ReplicateAudio'), ('ml_model.runners:ReplicateImageRunner', 'ReplicateImage'), ('ml_model.runners:ReplicateTextRunner', 'ReplicateText'), ('ml_model.runners:ReplicateVideoRunner', 'ReplicateVideo')], max_length=100, verbose_name='Runner'), + ), + migrations.AlterField( + model_name='modelinput', + name='deployment', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='deployment_inputs', to='ml_model.deployment', verbose_name='Deployment'), + ), + migrations.AlterField( + model_name='modelparameter', + name='deployment', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='deployment_parameters', to='ml_model.deployment', verbose_name='Deployment'), + ), + migrations.AlterField( + model_name='modelpaymentrule', + name='inference', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_paymentrules', to='ml_model.inference', verbose_name='Inference'), + ), + migrations.AlterField( + model_name='modelstat', + name='inference', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_tracking_records', to='ml_model.inference', verbose_name='Inference'), + ), + migrations.DeleteModel( + name='ModelCategory', + ), + migrations.RemoveField( + model_name='modelinput', + name='model', + ), + migrations.RemoveField( + model_name='modelinput', + name='versions', + ), + migrations.RemoveField( + model_name='modelparameter', + name='model', + ), + migrations.RemoveField( + model_name='modelparameter', + name='versions', + ), + migrations.RemoveField( + model_name='modelpaymentrule', + name='model', + ), + migrations.RemoveField( + model_name='modelpaymentrule', + name='versions', + ), + migrations.RemoveField( + model_name='modelstat', + name='model', + ), + migrations.RemoveField( + model_name='neuronmodel', + name='tags', + ), + migrations.RemoveField( + model_name='modelversion', + name='model', + ), + migrations.DeleteModel( + name='ModelVersion', + ), + migrations.RenameModel( + old_name='ModelTag', + new_name='Tag', + ), + migrations.RenameModel( + old_name='ModelInput', + new_name='Input', + ), + migrations.RenameModel( + old_name='ModelParameter', + new_name='Parameter', + ), + migrations.RenameModel( + old_name='ModelPaymentRule', + new_name='PaymentRule', + ), + migrations.RenameModel( + old_name='ModelStat', + new_name='TrackingRecord', + ), + migrations.CreateModel( + name='PaymentBias', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('coefficient', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Coefficient')), + ('type', models.CharField(choices=[('addition', 'Addition'), ('multiplication', 'Multiplication')], max_length=32, verbose_name='Type')), + ('inference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_payment_biases', to='ml_model.inference', verbose_name='Inference')), + ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), + ], + options={ + 'ordering': ('order',), + 'verbose_name': 'Payment Bias', + 'verbose_name_plural': 'Payment Bias', + }, + ), + migrations.DeleteModel( + name='PaymentRule', + ), + migrations.AlterModelOptions( + name='input', + options={'verbose_name': 'Input', 'verbose_name_plural': 'Inputs'}, + ), + migrations.AlterModelOptions( + name='parameter', + options={'verbose_name': 'Parameter', 'verbose_name_plural': 'Parameters'}, + ), + migrations.AlterModelOptions( + name='tag', + options={'verbose_name': 'Tag', 'verbose_name_plural': 'Tags'}, + ), + migrations.AlterModelOptions( + name='trackingrecord', + options={'ordering': ('-created_at',), 'verbose_name': 'Tracking Record', 'verbose_name_plural': 'Tracking Records'}, + ), + migrations.RemoveField( + model_name='parameter', + name='order', + ), + migrations.AddField( + model_name='inference', + name='tags', + field=models.ManyToManyField(blank=True, related_name='inferences_tags', to='ml_model.tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='inference', + name='name', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='parameter', + name='type', + field=models.CharField(choices=[('int', 'Integer'), ('float', 'Float'), ('str', 'String'), ('choices', 'Choices'), ('floatrange', 'Float range'), ('intrange', 'Integer range'), ('bool', 'Logical')], max_length=40, verbose_name='Type'), + ), + migrations.AlterField( + model_name='trackingrecord', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), + ), + migrations.AlterField( + model_name='trackingrecord', + name='generation_time', + field=models.DurationField(verbose_name='Generation time'), + ), + migrations.AlterField( + model_name='trackingrecord', + name='tokens_cost', + field=models.DecimalField(decimal_places=10, max_digits=50, verbose_name='Tokens cost'), + ), + migrations.CreateModel( + name='OverridenParameter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.JSONField(verbose_name='Value')), + ('hidden', models.BooleanField(default=False, verbose_name='Hidden')), + ('required', models.BooleanField(default=False, verbose_name='Required')), + ('inference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inference_parameters', to='ml_model.inference', verbose_name='Inference')), + ('parameter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters_overriden', to='ml_model.parameter', verbose_name='Parameter')), + ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), + ], + options={ + 'ordering': ('order',), + 'verbose_name': 'Overriden Parameter', + 'verbose_name_plural': 'Overriden Parameters', + 'unique_together': {('parameter', 'inference')}, + }, + ), + migrations.CreateModel( + name='PaymentRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('strategy', models.CharField(choices=[('fixed', 'Fixed'), ('per-second', 'Per generation second'), ('per-text-token', 'Per one text token'), ('per-pixel', 'Per image pixel')], max_length=32, verbose_name='Strategy')), + ('cost', models.DecimalField(decimal_places=8, help_text='In RUB, per specified strategy', max_digits=10, verbose_name='Cost')), + ('deployment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='deployment_payment_rules', to='ml_model.deployment', verbose_name='Deployment')), + ('content_type', models.CharField(blank=True, choices=[('text', 'Text'), ('file', 'File'), ('embeddings', 'Embeddings')], max_length=32, null=True, verbose_name='Content Type')), + ('interaction_type', models.CharField(blank=True, choices=[('input', 'By input data'), ('output', 'By output data')], max_length=32, null=True, verbose_name='Interaction Type')), + ], + options={ + 'verbose_name': 'Payment Rule', + 'verbose_name_plural': 'Payment Rules', + }, + ), + ] @@ -3,6 +3,7 @@ from ml_model.runners.openrouter import OpenrouterRunner from ml_model.runners.replicate import ( ReplicateAudioRunner, ReplicateImageRunner, + ReplicateIconicRunner, ReplicateTextRunner, ReplicateVideoRunner, ) @@ -14,6 +15,7 @@ __all__ = [ 'ReplicateTextRunner', 'ReplicateAudioRunner', 'ReplicateImageRunner', + 'ReplicateIconicRunner', 'ReplicateVideoRunner', 'FalAIRunner' ] @@ -21,10 +21,14 @@ class FalAIRunner(BaseRunner): scrape_results: list[StringIO] | list[BytesIO] = [], ) -> Generator[str, Any, None]: try: + image_size = { + "width": parameters.pop('width'), + "height": parameters.pop('height') + } version = parameters.pop('model') model_owner = parameters.pop('model_owner') official = version and model_owner - payload = {'prompt': content, **parameters} + payload = {'prompt': content, 'image_size': image_size, **parameters} if not official: payload.update({'model_version': version}) with httpx.Client( @@ -42,6 +42,7 @@ class OpenAICompatibleRunner(BaseRunner, ABC): 'stream': True, **parameters, } + logger.info(payload) for result in scrape_results: if isinstance(result, StringIO): payload['messages'].append( @@ -79,6 +80,7 @@ class OpenAICompatibleRunner(BaseRunner, ABC): try: with client.stream('POST', '/chat/completions', json=payload) as stream: stream_content = stream.iter_text() + logger.info(stream_content) if stream.status_code >= 400: raw = ''.join([chunk for chunk in stream_content]) @@ -108,6 +110,8 @@ class OpenAICompatibleRunner(BaseRunner, ABC): yield content except json.decoder.JSONDecodeError: continue + except httpx.ConnectError: + continue except httpx.TimeoutException as exc: logger.exception(exc) raise Exception('Timeout happened') @@ -1,8 +1,11 @@ +import logging + from django.conf import settings from ml_model.exceptions import ParameterNotValid from ml_model.runners.openai import OpenAICompatibleRunner +logger = logging.getLogger(__name__) class OpenrouterRunner(OpenAICompatibleRunner): BASE_URL = 'https://openrouter.ai/api/v1' @@ -13,6 +16,7 @@ class OpenrouterRunner(OpenAICompatibleRunner): @classmethod def map_errors(cls, error): if isinstance(error, str) or error['code'] == 400: + logger.error(error) raise Exception('Unexpected error') elif error['message'].endswith('is not a valid model ID'): raise ParameterNotValid('model') @@ -1,4 +1,5 @@ import base64 +import logging from abc import ABC, abstractmethod from io import BytesIO, StringIO from typing import TYPE_CHECKING, Any, Iterable @@ -12,6 +13,7 @@ from ml_model.runners.base import BaseRunner if TYPE_CHECKING: from messages.models import Message +logger = logging.getLogger(__name__) class ReplicateBaseRunner(BaseRunner, ABC): @classmethod @@ -131,6 +133,7 @@ class ReplicateImageRunner(BaseRunner): model_owner = parameters.pop('model_owner') official = version and model_owner payload = {'input': {parameters.pop('prompt_key', None) or 'prompt': content, **parameters}} + logger.info(official) if not official: payload.update({'version': version}) if file: @@ -152,9 +155,12 @@ class ReplicateImageRunner(BaseRunner): resp = client.post( f'/models/{model_owner}/{version}/predictions' if official else '/predictions', json=payload ) + logger.info(resp) data = resp.json() + logger.info(data) stream_url = data['urls']['stream'] get_url = data['urls']['get'] + logger.info(stream_url, get_url) with client.stream( 'GET', stream_url, headers={'Accept': 'text/event-stream', 'Cache-Control': 'no-store'} ) as stream: @@ -168,6 +174,7 @@ class ReplicateImageRunner(BaseRunner): if content_length == 0: resp = client.get(get_url) data = resp.json() + logger.info(data) if data['status'] == 'failed': raise Exception('Model failed generation') @@ -180,3 +187,56 @@ class ReplicateVideoRunner(BaseRunner): class ReplicateAudioRunner(BaseRunner): @classmethod def generate(cls, content=None, file=None, parameters={}, history=[], scrape_results=[]): ... + + +class ReplicateIconicRunner(BaseRunner): + @classmethod + def generate(cls, content=None, file=None, parameters={}, history=[], scrape_results=[]): + version = parameters.pop('version') + model_owner = parameters.pop('model_owner') + official = version and model_owner + payload = {'input': {parameters.pop('prompt_key', None) or 'prompt': content, **parameters}} + logger.info(official) + if not official: + payload.update({'version': version}) + if file: + kind = filetype.guess(file.read(20)) + format = 'jpeg' if kind.extension == 'jpg' else kind.extension + if format in ('jpeg', 'png'): + mime = kind.mime if kind else 'application/octet-stream' + payload['input'][parameters.pop('file_key', None) or 'file'] = ( + f'data:{mime};base64,{base64.b64encode(file.read()).decode("utf-8")}' + ) + with httpx.Client( + base_url='https://api.replicate.com/v1', + headers={ + 'Authorization': f'Bearer {settings.REPLICATE_API_KEY}', + 'Content-Type': 'application/json', + }, + timeout=None, + ) as client: + resp = client.post( + f'/models/{model_owner}/{version}/predictions' if official else '/predictions', json=payload + ) + logger.info(resp) + data = resp.json() + logger.info(data) + stream_url = data['urls']['stream'] + get_url = data['urls']['get'] + logger.info(stream_url, get_url) + with client.stream( + 'GET', stream_url, headers={'Accept': 'text/event-stream', 'Cache-Control': 'no-store'} + ) as stream: + content_length = 0 + for chunk in stream.iter_text(): + chunk = chunk.strip().split('\n') + chunk = chunk[0] if content_length > 0 else chunk[-1][len('data:') :].strip() + content_length += len(chunk) + yield chunk + + if content_length == 0: + resp = client.get(get_url) + data = resp.json() + logger.info(data) + if data['status'] == 'failed': + raise Exception('Model failed generation') @@ -29,3 +29,11 @@ class PaymentRuleNotImplemented(Exception): class ScraperDoesNotExists(Exception): def __str__(self): return _('Scraper does not exists') + + +class FormatNotSupported(Exception): + def __init__(self, formats: str) -> None: + self.formats = formats + + def __str__(self) -> str: + return _('Format is not supported. Supported formats: %(formats)s') % {'formats': self.formats} @@ -1,3 +1,4 @@ +import asyncio import logging from asgiref.sync import async_to_sync @@ -7,6 +7,9 @@ services: dockerfile: Dockerfile volumes: - static:/code/static + networks: + - default + - infrastructure command: - /bin/sh - -c @@ -51,7 +54,7 @@ services: command: - /bin/sh - -c - - python manage.py migrate + - python manage.py migrate --verbosity 3 env_file: - $ENV @@ -59,6 +62,8 @@ services: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA command: celery -A backend worker -l INFO --concurrency 8 + networks: + - default env_file: - $ENV environment: @@ -70,6 +75,8 @@ services: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA command: celery -A backend beat -l INFO + networks: + - default env_file: - $ENV depends_on: @@ -101,17 +108,22 @@ services: cache-mdb: image: redis:alpine + networks: + - default restart: unless-stopped celery-mdb: image: redis:alpine + networks: + - default restart: unless-stopped networks: - default: + infrastructure: name: infrastructure external: true - + default: {} + volumes: static: name: "backend-static" @@ -10,10 +10,6 @@ services: 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 volumes: - static:/code/static command: