@@ -214,7 +214,7 @@ class NewBusinessHostSerializer(serializers.Serializer): usage_intensity = serializers.ChoiceField(choices=UsageIntensity.choices) ITN = serializers.IntegerField() PSRN = serializers.IntegerField() - company_name = serializers.CharField() + company_name = serializers.CharField(allow_blank=True) preferred_name = serializers.CharField(required=False) corporate_email = serializers.EmailField(required=False) corporate_phone = serializers.CharField(required=False) @@ -2,13 +2,13 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-04-09 17:28+0300\n" +"POT-Creation-Date: 2026-04-28 15:28+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -189,7 +189,7 @@ msgid "Child Business Accounts" msgstr "Дочерние Бизнес Аккаунты" #: authentication/models/business_group.py:8 ml_model/models.py:17 -#: ml_model/models.py:37 ml_model/models.py:61 ml_model/models.py:268 +#: ml_model/models.py:37 ml_model/models.py:61 ml_model/models.py:269 #: tools/chats/models.py:9 tools/media/models.py:59 tools/media/models.py:95 msgid "Title" msgstr "Название" @@ -206,8 +206,8 @@ msgstr "Бизнес Группы" #: authentication/models/email_token.py:13 authentication/models/user.py:222 #: authentication/models/user.py:223 authentication/models/user_telegram.py:22 #: authentication/models/user_vk.py:12 payments/admin.py:37 -#: payments/admin.py:95 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:50 +#: payments/admin.py:98 payments/models/invoice.py:15 +#: payments/models/payment.py:26 payments/models/payment_plan.py:62 #: tools/media/models.py:108 msgid "User" msgstr "Пользователь" @@ -363,7 +363,7 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:16 ml_model/models.py:270 +#: authentication/models/email_token.py:16 ml_model/models.py:271 msgid "Key" msgstr "Ключ" @@ -624,7 +624,7 @@ msgstr "" msgid "A %(model)s with fields %(fields)s already exists" msgstr "Уже существует %(model)s с полями %(fields)s" -#: messages/serializers.py:44 ml_model/exceptions.py:68 +#: messages/serializers.py:44 ml_model/exceptions.py:78 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" @@ -684,63 +684,76 @@ msgstr "Возможно, файл повреждён. Попробуйте за msgid "File Uploading Not supported" msgstr "Загрузка файлов не поддерживается" -#: ml_model/exceptions.py:65 +#: ml_model/exceptions.py:70 msgid "Unable to recognize the file" msgstr "Не удаётся распознать файл" -#: ml_model/exceptions.py:73 +#: ml_model/exceptions.py:83 msgid "The length of the context has been exceeded." msgstr "Длина контекста превышена." -#: ml_model/exceptions.py:78 +#: ml_model/exceptions.py:88 msgid "Jinja template not found" msgstr "Jinja-шаблон не найден" -#: ml_model/exceptions.py:83 +#: ml_model/exceptions.py:93 msgid "There was an unknown error while rendering a template" msgstr "При рендеринге шаблона произошла неизвестная ошибка" -#: ml_model/exceptions.py:88 +#: ml_model/exceptions.py:98 msgid "The neuron model does not exist" msgstr "Нейронная модель не существует" -#: ml_model/exceptions.py:96 +#: ml_model/exceptions.py:106 #, python-format msgid "The %(file_type)s is not attached" msgstr "Файл (%(file_type)s) не прикреплен" -#: ml_model/exceptions.py:101 +#: ml_model/exceptions.py:111 msgid "No image content found in response. Try a different request" msgstr "В промпте отсутствует описание изображения. Попробуйте другой запрос" -#: ml_model/exceptions.py:106 +#: ml_model/exceptions.py:116 +msgid "" +"The model could not analyze your request. Please rephrase it and try again" +msgstr "" +"Модель не смогла проанализировать ваш запрос. Перефразируйте его и " +"попробуйте снова" + +#: ml_model/exceptions.py:121 msgid "Image analysis error. Please try another image." msgstr "Ошибка анализа изображения. Попробуйте другую картинку." -#: ml_model/exceptions.py:111 +#: ml_model/exceptions.py:126 msgid "Use style type AUTO or GENERAL when a style preset is selected" msgstr "При выбранном стиле используйте тип стиля AUTO или GENERAL" -#: ml_model/exceptions.py:116 +#: ml_model/exceptions.py:131 msgid "Prediction interrupted. Please retry again" msgstr "Генерация прервана. Пожалуйста, повторите попытку еще раз" -#: ml_model/exceptions.py:131 +#: ml_model/exceptions.py:146 #, python-format msgid "Prompt is too long. Maximum length is %(max_length)s characters." msgstr "Промпт слишком длинный. Максимальная длина — %(max_length)s символов." -#: ml_model/exceptions.py:138 +#: ml_model/exceptions.py:153 msgid "" "Service is currently unavailable due to high demand. Please try again later" msgstr "" "Сервис временно недоступен из-за высокой нагрузки. Пожалуйста, попробуйте " "позже" -#: ml_model/exceptions.py:143 +#: ml_model/exceptions.py:158 msgid "Available only in paid plan" msgstr "Доступно только в платном тарифе" +#: ml_model/exceptions.py:163 +#, fuzzy +#| msgid "Image analysis error. Please try another image." +msgid "Face not found in the image. Please try another image with a face." +msgstr "Не найдено лицо на картинке. Попробуйте другую картинку с лицом." + #: ml_model/models.py:18 ml_model/models.py:38 ml_model/models.py:70 #: ml_model/models.py:182 tools/media/models.py:67 msgid "Slug" @@ -774,7 +787,7 @@ msgstr "Теги модели" msgid "Alternative Titles" msgstr "Альтернативные названия" -#: ml_model/models.py:68 ml_model/models.py:181 ml_model/models.py:269 +#: ml_model/models.py:68 ml_model/models.py:181 ml_model/models.py:270 #: payments/models/payment.py:52 msgid "Description" msgstr "Описание" @@ -795,7 +808,7 @@ msgstr "Теги" msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:156 ml_model/models.py:402 payments/admin.py:101 +#: ml_model/models.py:156 ml_model/models.py:403 payments/admin.py:104 msgid "Model" msgstr "Модель" @@ -861,162 +874,166 @@ msgstr "ZIP архив" msgid "Audio" msgstr "Аудио" -#: ml_model/models.py:234 ml_model/models.py:272 +#: ml_model/models.py:229 payments/tests/test_plans.py:31 +msgid "Video" +msgstr "Видео" + +#: ml_model/models.py:235 ml_model/models.py:273 #: payments/models/promocode.py:41 msgid "Type" msgstr "Тип" -#: ml_model/models.py:236 ml_model/models.py:283 +#: ml_model/models.py:237 ml_model/models.py:284 msgid "Required" msgstr "Обязательный" -#: ml_model/models.py:239 +#: ml_model/models.py:240 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "%(model_title)s | %(input_type)s" -#: ml_model/models.py:245 +#: ml_model/models.py:246 msgid "Model Input" msgstr "Модель" -#: ml_model/models.py:246 +#: ml_model/models.py:247 msgid "Model Inputs" msgstr "Входящий поток модели" -#: ml_model/models.py:251 +#: ml_model/models.py:252 msgid "Integer" msgstr "Целое число" -#: ml_model/models.py:252 +#: ml_model/models.py:253 msgid "Float" msgstr "Вещественное число" -#: ml_model/models.py:253 +#: ml_model/models.py:254 msgid "String" msgstr "Строка" -#: ml_model/models.py:256 +#: ml_model/models.py:257 msgid "List" msgstr "Список" -#: ml_model/models.py:260 +#: ml_model/models.py:261 msgid "Float range" msgstr "Вещественный диапазон" -#: ml_model/models.py:264 +#: ml_model/models.py:265 msgid "Integer range" msgstr "Целочисленный диапазон" -#: ml_model/models.py:266 +#: ml_model/models.py:267 msgid "Logical" msgstr "Логический" -#: ml_model/models.py:279 +#: ml_model/models.py:280 msgid "Values" msgstr "Значения" -#: ml_model/models.py:280 +#: ml_model/models.py:281 msgid "" "These values can contain different interfaces and default value optional" msgstr "" "Значения могут содержать различные интерфейс и, опционально, значение по " "умолчанию" -#: ml_model/models.py:282 +#: ml_model/models.py:283 msgid "Hidden" msgstr "Скрытый" -#: ml_model/models.py:288 +#: ml_model/models.py:289 #, python-format msgid "Parameter of %(model_title)s" msgstr "Параметр %(model_title)s" -#: ml_model/models.py:291 +#: ml_model/models.py:292 msgid "Parameter" msgstr "Параметр" -#: ml_model/models.py:292 +#: ml_model/models.py:293 msgid "Parameters" msgstr "Параметры" -#: ml_model/models.py:297 +#: ml_model/models.py:298 msgid "Fixed" msgstr "Фикса" -#: ml_model/models.py:298 +#: ml_model/models.py:299 msgid "Per generation second" msgstr "За секунду генерации" -#: ml_model/models.py:299 +#: ml_model/models.py:300 msgid "Per one text token" msgstr "За один текстовый токен" -#: ml_model/models.py:300 +#: ml_model/models.py:301 msgid "Per image pixel" msgstr "За один пиксель" -#: ml_model/models.py:303 +#: ml_model/models.py:304 msgid "By input data" msgstr "По входящим данным" -#: ml_model/models.py:304 +#: ml_model/models.py:305 msgid "By output data" msgstr "По исходящим данным" -#: ml_model/models.py:305 +#: ml_model/models.py:306 msgid "By all data" msgstr "По всем данным" -#: ml_model/models.py:310 +#: ml_model/models.py:311 msgid "Strategy" msgstr "Стратегия" -#: ml_model/models.py:315 +#: ml_model/models.py:316 msgid "Interaction Type" msgstr "Тип взаимодействия" -#: ml_model/models.py:320 payments/models/invoice.py:19 +#: ml_model/models.py:321 payments/models/invoice.py:19 msgid "Cost" msgstr "Цена" -#: ml_model/models.py:321 +#: ml_model/models.py:322 msgid "In RUB, per specified strategy" msgstr "В рублях, за указанную стратегию" -#: ml_model/models.py:326 +#: ml_model/models.py:327 msgid "Coefficient" msgstr "Коэффициент" -#: ml_model/models.py:327 +#: ml_model/models.py:328 msgid "Cost multiplier" msgstr "Цена" -#: ml_model/models.py:334 +#: ml_model/models.py:335 msgid "Rate" msgstr "Ставка" -#: ml_model/models.py:338 +#: ml_model/models.py:339 msgid "Payment Rule" msgstr "Платежное правило" -#: ml_model/models.py:339 +#: ml_model/models.py:340 msgid "Payment Rules" msgstr "Платежные правила" -#: ml_model/models.py:400 +#: ml_model/models.py:401 msgid "Descriptor" msgstr "Дескриптор" -#: ml_model/models.py:406 +#: ml_model/models.py:407 #, python-format msgid "Instruction of %(model_title)s" msgstr "Инструкция %(model_title)s" -#: ml_model/models.py:409 +#: ml_model/models.py:410 msgid "Model Instruction" msgstr "Инструкция Модели" -#: ml_model/models.py:410 +#: ml_model/models.py:411 msgid "Model Instructions" msgstr "Инструкции Моделей" @@ -1024,7 +1041,7 @@ msgstr "Инструкции Моделей" msgid "no model by this id" msgstr "Не найдено моделей по этому ID" -#: ml_model/services/chatgpt.py:148 +#: ml_model/services/chatgpt.py:154 msgid "No matching version found" msgstr "Соответствующая версия не найдена" @@ -1032,6 +1049,20 @@ msgstr "Соответствующая версия не найдена" msgid "Image is ready" msgstr "Изображение готово" +#: ml_model/services/hunyuan.py:103 +#, python-format +msgid "This video duration is not allowed for %(quality)s quality." +msgstr "" +"Для качества %(quality)s такая продолжительность видео не поддерживается." + +#: ml_model/services/hunyuan.py:108 +msgid "" +"Smooth motion mode is available only for 5-second videos at 540p and 720p " +"quality" +msgstr "" +"Режим «Плавное движение» доступен только для 5-секундных видео в качестве " +"540p и 720p" + #: ml_model/services/minimaxmusic.py:58 #: ml_model/services/minimaxmusic_lite.py:62 msgid "Lyrics is too long" @@ -1050,13 +1081,13 @@ msgstr "Нет изображения для улучшения" msgid "Model data cannot be retrieved" msgstr "Невозможно получить данные модели" -#: payments/admin.py:35 payments/admin.py:67 payments/admin.py:93 +#: payments/admin.py:35 payments/admin.py:70 payments/admin.py:96 msgid "You can search by user email, exacted company name" msgstr "" "Вы можете осуществлять поиск по e-mail пользователя, точному названию " "компании" -#: payments/admin.py:40 payments/admin.py:98 +#: payments/admin.py:40 payments/admin.py:101 msgid "Missing" msgstr "Отсутствующий" @@ -1284,10 +1315,6 @@ msgstr "Чат-боты" msgid "Images" msgstr "Изображения" -#: payments/tests/test_plans.py:31 -msgid "Video" -msgstr "Видео" - #: poller/models.py:11 msgid "Address" msgstr "Адрес" @@ -1324,7 +1351,7 @@ msgstr "Публичный API" msgid "Media" msgstr "Медиа" -#: tools/chats/apis.py:197 tools/media/apis.py:201 +#: tools/chats/apis.py:201 tools/media/apis.py:207 #: tools/public_api/views/base.py:100 msgid "" "An unexpected generation error has occurred. Please try again later or use a " @@ -1333,7 +1360,7 @@ msgstr "" "Произошла непредвиденная ошибка при генерации. Пожалуйста попробуйте позже " "или используйте другую модель" -#: tools/chats/apis.py:253 +#: tools/chats/apis.py:257 msgid "The message has already been deleted" msgstr "Сообщение уже было удалено" @@ -1346,13 +1373,12 @@ msgstr "Чат %(id)s" msgid "Chat" msgstr "Чат" -#: tools/media/apis.py:172 +#: tools/media/apis.py:175 msgid "" "Temporary issues with the service, we are already working on a solution." msgstr "Временные неполадки с сервисом, мы уже работаем над их решением." -#: tools/media/apis.py:248 tools/media/apis.py:283 -#: tools/public_api/views/ml_service.py:63 +#: tools/media/apis.py:254 tools/public_api/views/ml_service.py:63 #: tools/public_api/views/ml_service.py:107 msgid "Voice not found." msgstr "Голос не найден." @@ -1365,6 +1391,7 @@ msgstr "Хранилище клонирования голоса" msgid "Voice clone stores" msgstr "Хранилища клонирования голоса" +#: tools/media/models.py:54 tools/media/models.py:121 #: tools/media/models.py:54 tools/media/models.py:121 msgid "Voice" msgstr "Голос" @@ -1377,6 +1404,7 @@ msgstr "Инструментал" msgid "Kind" msgstr "Тип" +#: tools/media/models.py:72 tools/media/models.py:105 #: tools/media/models.py:72 tools/media/models.py:105 msgid "File" msgstr "Файл" @@ -1401,22 +1429,36 @@ msgstr "" msgid "Изменён" msgstr "" +#: tools/media/models.py:102 +#: tools/media/models.py:93 +msgid "Создан" +msgstr "" + +#: tools/media/models.py:94 +msgid "Изменён" +msgstr "" + #: tools/media/models.py:102 msgid "Only MP3, OGG, WAV and WEBA audio files are allowed." msgstr "Разрешены только аудиофайлы MP3, OGG, WAV и WEBA." +#: tools/media/models.py:110 #: tools/media/models.py:110 msgid "Transcription" msgstr "Транскрипция" +#: tools/media/models.py:114 #: tools/media/models.py:114 msgid "Unknown file" msgstr "Неизвестный файл" +#: tools/media/models.py:122 #: tools/media/models.py:122 msgid "Voices" msgstr "Голоса" +#: tools/media/routes/v1.py:76 tools/public_api/views/voice.py:86 +#: tools/public_api/views/voice.py:98 #: tools/media/routes/v1.py:76 tools/public_api/views/voice.py:86 #: tools/public_api/views/voice.py:98 msgid "Voice not found" @@ -1456,18 +1498,22 @@ msgstr "" msgid "The request must not be empty" msgstr "Запрос не должен быть пустым" +#: tools/public_api/views/ml_service.py:128 #: tools/public_api/views/ml_service.py:128 msgid "You must provide a model parameter" msgstr "Необходимо указать параметр 'model'" +#: tools/public_api/views/ml_service.py:143 #: tools/public_api/views/ml_service.py:143 msgid "Missing required parameter: 'messages'" msgstr "Отсутствует обязательный параметр: 'messages'" +#: tools/public_api/views/ml_service.py:193 #: tools/public_api/views/ml_service.py:193 msgid "Model not found" msgstr "Модель не найдена" +#: tools/public_api/views/voice.py:44 #: tools/public_api/views/voice.py:44 msgid "Your voice has been uploaded successfully" msgstr "Ваш голос успешно загружен" @@ -1477,10 +1523,17 @@ msgid "Preset voices are shared and cannot be edited. Use your own voice id." msgstr "" "Пресеты общие для всех — их нельзя редактировать. Укажите id своего голоса." +#: tools/public_api/views/voice.py:87 +#: tools/public_api/views/voice.py:77 +msgid "Preset voices are shared and cannot be edited. Use your own voice id." +msgstr "" +"Пресеты общие для всех — их нельзя редактировать. Укажите id своего голоса." + #: tools/public_api/views/voice.py:87 msgid "Voice title updated successfully" msgstr "Название голоса успешно обновлено" +#: tools/public_api/views/voice.py:93 #: tools/public_api/views/voice.py:93 msgid "Preset voices are shared and cannot be deleted. Use your own voice id." msgstr "Пресеты общие для всех — их нельзя удалить. Укажите id своего голоса." @@ -1562,16 +1615,6 @@ msgstr "Пресеты общие для всех — их нельзя удал #~ msgid "Issued achievement" #~ msgstr "Выданное достижение" -msgid "Smooth motion mode is available only for 5-second videos at 540p and 720p quality" -msgstr "Режим «Плавное движение» доступен только для 5-секундных видео в качестве 540p и 720p" - -#, python-format -msgid "This video duration is not allowed for %(quality)s quality." -msgstr "Для качества %(quality)s такая продолжительность видео не поддерживается." - -msgid "The model could not analyze your request. Please rephrase it and try again" -msgstr "Модель не смогла проанализировать ваш запрос. Перефразируйте его и попробуйте снова" - #~ msgid "Account is already confirmed" #~ msgstr "Аккаунт уже подтвержден" @@ -7,23 +7,24 @@ from ml_model.services.dalle import Dalle from ml_model.services.deepl import Deepl 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.elevenlabs_music import Elevenlabs_Music from ml_model.services.elevenlabs import Elevenlabs +from ml_model.services.elevenlabs_music import Elevenlabs_Music +from ml_model.services.epicphotogasm import Epicphotogasm from ml_model.services.flux import Flux from ml_model.services.flux_2 import Flux_2 from ml_model.services.fluxkrea import Fluxkrea from ml_model.services.fluxlorafast import Fluxlorafast from ml_model.services.fluxproultra import Fluxproultra +from ml_model.services.fluxpulid import Fluxpulid from ml_model.services.gemini import Gemini from ml_model.services.gemini_3_1 import Gemini_3_1 -from ml_model.services.gemma import Gemma from ml_model.services.geminiimage import Geminiimage +from ml_model.services.gemma import Gemma from ml_model.services.gptimage import Gptimage from ml_model.services.granite import Granite from ml_model.services.grok import Grok -from ml_model.services.grok_image import Grok_Image from ml_model.services.grok_4_1_fast import Grok_4_1_Fast +from ml_model.services.grok_image import Grok_Image from ml_model.services.grok_imagine_video import Grok_Imagine_Video from ml_model.services.hailuo import Hailuo from ml_model.services.hunyuan import Hunyuan @@ -36,22 +37,22 @@ from ml_model.services.leonardo import Leonardo from ml_model.services.lightning import Lightning from ml_model.services.llama import Llama from ml_model.services.logoai import Logoai -from ml_model.services.lyria import Lyria from ml_model.services.ltx import Ltx +from ml_model.services.lyria import Lyria from ml_model.services.midjourney import Midjourney -from ml_model.services.minimaxvideo import Minimaxvideo from ml_model.services.minimaxmusic import Minimaxmusic from ml_model.services.minimaxmusic_lite import Minimaxmusic_Lite +from ml_model.services.minimaxvideo import Minimaxvideo from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen from ml_model.services.nanobanana import Nanobanana from ml_model.services.nanobanana_2 import Nanobanana_2 from ml_model.services.perplexity import Perplexity -from ml_model.services.pulid import Pulid from ml_model.services.photon import Photon from ml_model.services.pixverse import Pixverse -from ml_model.services.prunaai import Prunaai from ml_model.services.pruna_v import Pruna_V +from ml_model.services.prunaai import Prunaai +from ml_model.services.pulid import Pulid from ml_model.services.qwen import Qwen from ml_model.services.qwen_235B import Qwen_235B from ml_model.services.qwen_3_5 import Qwen_3_5 @@ -62,9 +63,9 @@ from ml_model.services.recraft import Recraft from ml_model.services.reve import Reve from ml_model.services.runway import Runway from ml_model.services.sdxlemoji import Sdxlemoji +from ml_model.services.seedance import Seedance from ml_model.services.seedream import Seedream from ml_model.services.sora import Sora -from ml_model.services.seedance import Seedance from ml_model.services.stablediffusion import Stablediffusion from ml_model.services.stablemusic import Stablemusic from ml_model.services.suno import Suno @@ -0,0 +1,91 @@ +from datetime import timedelta +from decimal import Decimal +from io import BytesIO +import time +from typing import Any + +from django.core.files import File +from replicate.exceptions import ModelError +import requests + +from messages.models.message import Message +from ml_model.exceptions import FaceNotFoundError, GenerationException, RequestBlocked +from ml_model.models import ModelParameter +from ml_model.services.base import SimpleService +from ml_model.tasks import replicate_run +from payments.exceptions.insufficient_balance import InsufficientBalance +from payments.selectors.payment_plan_selector import PaymentPlanSelector + + +class Fluxpulid(SimpleService): + TOKEN_COST = Decimal('15.0') + ENDPOINT = 'bytedance/flux-pulid:8baa7ef2255075b46f4d91cd238c21d31181b3e6a864463f967960bb0112525b' + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = info.get('num_outputs', 1) * cls.TOKEN_COST + + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def calculate_price(self, num_outputs: int) -> Decimal: + price = num_outputs * self.TOKEN_COST + + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, content: str, images: list, t: timedelta, save: bool = True) -> list[Message]: + messages: list[Message] = [] + for image in images: + messages.append( + Message( + content_object=self.store, + elapsed_time=t, + content=content, + file=File(BytesIO(requests.get(image.url).content), '.png'), + ) + ) + + if save: + return Message.objects.bulk_create(messages) + + return messages + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + num_outputs = input_message.info.get('num_outputs', 1) + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.TOKEN_COST * num_outputs + ): + raise InsufficientBalance(balance, cost) + + size = input_message.info.pop('size', '1024x1024') + width, height = size.split('x') + + translated_prompt = self.translate_prompt(input_message.content) + callback_data = dict( + { + 'prompt': translated_prompt, + 'main_face_image': BytesIO(input_message.file.read()), + 'output_quality': 100, + 'output_format': 'png', + 'max_sequence_length': 256, + 'width': width, + 'height': height, + **input_message.info, + } + ) + start_time = time.time() + try: + images = replicate_run(self.ENDPOINT, callback_data) + except ModelError as exc: + if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): + raise RequestBlocked + elif exc.prediction.error == 'facexlib align face fail': + raise FaceNotFoundError + raise GenerationException from exc + + process_time = timedelta(seconds=(time.time() - start_time)) + if num_outputs == 1: + images = [images] + self.handle_invoice(input_message.content_object.model, len(images)) + msgs = self.save_results(input_message.content, images, process_time, save) + + return msgs @@ -1,77 +1,119 @@ import base64 import logging +import math import time from datetime import timedelta from decimal import Decimal from io import BytesIO +import filetype import httpx +import tiktoken from django.core.files import File +from django.core.files.images import get_image_dimensions from backend import settings from messages.models import BaseStore, Message -from ml_model.exceptions import RequestBlocked, GenerationException +from ml_model.exceptions import ( + CorruptedFileError, + FileExtensionNotSupported, + GenerationException, + RequestBlocked, +) from ml_model.services.base import SimpleService +from payments.exceptions.insufficient_balance import InsufficientBalance +from payments.selectors.payment_plan_selector import PaymentPlanSelector from poller.models import Proxy class Gptimage(SimpleService): - """ - GPTImage Service - contains abstract method make, which makes a generation - """ - TOKENS_COST = { - 'gpt-image-1': { + 'gpt-image-2': { 'input': Decimal('0.0025'), 'image': { - 'input': Decimal('0.005'), - 'output': Decimal('0.02'), - 'generation': { - 'low': { - '1024x1024': Decimal('5.5'), - '1024x1536': Decimal('8'), - '1536x1024': Decimal('8'), - }, - 'medium': { - '1024x1024': Decimal('21'), - '1024x1536': Decimal('31.5'), - '1536x1024': Decimal('31.5'), - }, - 'high': { - '1024x1024': Decimal('83.5'), - '1024x1536': Decimal('125'), - '1536x1024': Decimal('125'), - }, - }, + 'input': Decimal('0.004'), + 'output': Decimal('0.015'), }, - } + }, + } + QUALITY = { + 'Низкое': 'low', + 'Высокое': 'medium', + } + SIZE_TOKENS = { + 'low': { + '1024x1024': 196, + '1536x1024': 158, + '1024x1536': 158, + }, + 'medium': { + '1024x1024': 1756, + '1536x1024': 1372, + '1024x1536': 1372, + }, } - def __init__(self, store: BaseStore) -> None: super().__init__(store) self.logger = logging.getLogger(self.__class__.__name__) + def calculate_price(self, text_tokens: int, image_tokens: int, output_tokens: int) -> Decimal: + price = ( + text_tokens * self.TOKENS_COST['gpt-image-2']['input'] + + image_tokens * self.TOKENS_COST['gpt-image-2']['image']['input'] + + output_tokens * self.TOKENS_COST['gpt-image-2']['image']['output'] + ) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, prompt: str, image: bytes, time: timedelta, save: bool = True) -> list[Message]: + messages: list[Message] = [] + image_bytes = base64.b64decode(image) + messages.append( + Message( + content_object=self.store, + elapsed_time=time, + content=prompt, + file=File(BytesIO(image_bytes), '.png'), + ) + ) + if save: + return Message.objects.bulk_create(messages) + return messages + def make(self, input_message: Message, save: bool = True) -> list[Message]: info = input_message.info.copy() - files = None + size = info.get('size', '1024x1024') + quality = self.QUALITY[info.get('quality', 'Низкое')] + w, h = 0, 0 + if file := input_message.file: + file_bytes = file.read() + kind = filetype.guess(file_bytes[:20]) + if not kind: + raise CorruptedFileError + if kind.extension.upper() not in (extensions := ['PNG', 'JPG', 'JPEG', 'WEBP']): + raise FileExtensionNotSupported(extensions) + file.seek(0) + w, h = get_image_dimensions(file) + if not w or not h: + raise CorruptedFileError + files = {'image': ('image.png', BytesIO(file_bytes), 'image/png')} + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.calculate_price( + *self.count_predict_tokens(input_message.content, w, h, size, quality) + ) + + (1 if not input_message.file else 4) + ): + raise InsufficientBalance(balance, cost) for proxy in Proxy.objects.all(): + moderation = 'low' if self.store.user.account_type == 'regular' else 'auto' json_data = { 'prompt': input_message.content, - 'background': info.get('background', 'auto'), - 'model': 'gpt-image-1', + 'model': 'gpt-image-2', + 'n': 1, + 'quality': quality, + 'size': size, + 'moderation': moderation, 'output_format': 'png', - 'quality': info.get('quality', 'auto'), - 'size': info.get('size', 'auto'), } - if input_message.file: - version = 'image-edit' - file_bytes = input_message.file.read() - files = { - 'image': ('image.png', BytesIO(file_bytes), 'image/png'), - } - else: - version = 'image-generate' with httpx.Client( base_url='https://api.openai.com/v1', proxy=f'{proxy.protocol}://{proxy.address}', @@ -79,7 +121,7 @@ class Gptimage(SimpleService): timeout=600, ) as client: start_time = time.time() - if version == 'image-generate': + if not input_message.file: data = client.post('images/generations', json=json_data).json() else: data = client.post('images/edits', data=json_data, files=files).json() @@ -99,38 +141,25 @@ class Gptimage(SimpleService): msgs = self.save_results(input_message.content, image, process_time, save) return msgs - def calculate_price( - self, - text_tokens: int, - image_tokens: int, - output_tokens: int, - *args, - **kwargs, - ) -> Decimal: - price = ( - text_tokens * self.TOKENS_COST['gpt-image-1']['input'] - + image_tokens * self.TOKENS_COST['gpt-image-1']['image']['input'] - + output_tokens * self.TOKENS_COST['gpt-image-1']['image']['output'] - ) - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def count_predict_tokens( + cls, text_input: str, image_width: int, image_height: int, size: str, quality: str + ): + encoding = tiktoken.get_encoding('o200k_base') + text_input_tokens = len(encoding.encode(text_input)) + image_input_tokens = cls.count_image_tokens(image_width, image_height) + image_output_tokens = cls.SIZE_TOKENS[quality][size] + return text_input_tokens, image_input_tokens, image_output_tokens - def save_results( - self, - prompt: str, - image: bytes, - time: timedelta, - save: bool = True, - ) -> list[Message]: - messages: list[Message] = [] - image_bytes = base64.b64decode(image) - messages.append( - Message( - content_object=self.store, - elapsed_time=time, - content=prompt, - file=File(BytesIO(image_bytes), '.png'), - ) - ) - if save: - return Message.objects.bulk_create(messages) - return messages + @staticmethod + def count_image_tokens(w: int, h: int) -> int: + try: + p = math.ceil(w / 16) * math.ceil(h / 16) + r = max(w, h) / min(w, h) + s = 1521 * (1 - math.exp(-p / 1150.0)) + a = math.exp(-0.16 * (math.log(r) ** 2)) + k = 0.72 + 0.28 * (1 - math.exp(-p / 900.0)) + t = s * a * k + return int(round(max(16, min(1521, t)))) + except ZeroDivisionError: + return 0 @@ -156,3 +156,8 @@ class ServiceHighDemandError(Exception): class PaidPlanRequiredError(Exception): def __str__(self) -> str: return _('Available only in paid plan') + + +class FaceNotFoundError(Exception): + def __str__(self) -> str: + return _('Face not found in the image. Please try another image with a face.') @@ -13,6 +13,7 @@ from messages.serializers import MessageSerializer from ml_model.exceptions import ( CorruptedFileError, ExceededContextLengthError, + FaceNotFoundError, FileExtensionNotSupported, FileNotProvided, FileTooLargeError, @@ -196,6 +197,7 @@ class MediaAPIView(APIView): FileTooLargeError, ImageAnalysisError, UnrecognizedFileError, + FaceNotFoundError, ), ): return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) @@ -3,6 +3,9 @@ SECRET_KEY=testtest DEBUG=true STATIC_PATH_PREFIX=static +RELEASE=1.0.0 +ENVIRONMENT=dev + # NEURON MODELS OPENAI_API_KEY=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 STABLE_DIFFUSION_API_KEY=sk-fztQxZobaL0SD7PgpmK7XMQlyNivpKFZNJnqAVG2CcbvAP6Z @@ -25,7 +28,7 @@ UPSCALE_MULTIPLIER_HOST=packet:8080 JWT_SECRET_KEY=testtest JWT_ACCESS_TOKEN_LIFETIME=604800 JWT_REFRESH_TOKEN_LIFETIME=604800 -ALLOWED_HOSTS=localhost +ALLOWED_HOSTS=localhost, 127.0.0.1 CSRF_TRUSTED_ORIGINS=http://localhost CORS_ALLOWED_ORIGINS=http://localhost:3000 TELEGRAM_BOT_TOKEN=None @@ -10,6 +10,7 @@ default: build: stage: Build variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA script: - touch .env - docker compose build --push @@ -23,6 +24,7 @@ build: services: - docker:dind variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA ENVIRONMENT: $CI_ENVIRONMENT_TIER DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" @@ -1,5 +1,5 @@ x-app-config: &app-config - image: ${CI_REGISTRY_IMAGE:-air/backend}:${CI_COMMIT_SHA:-latest} + image: ${IMAGE_TAG:-air/backend:latest} restart: on-failure:3 environment: - RELEASE