@@ -477,6 +477,7 @@ if (SENTRY_URL := env.str('SENTRY_URL', '')) and RELEASE and ENVIRONMENT: 'RequestBlocked', 'PredictionInterruptedError', 'ImageContentNotFound', + 'PromptLengthExceeded' ], ) @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-25 15:54+0300\n" +"POT-Creation-Date: 2026-01-12 11:29+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -670,6 +670,11 @@ msgstr "При выбранном стиле используйте тип ст msgid "Prediction interrupted. Please retry again" msgstr "Генерация прервана. Пожалуйста, повторите попытку еще раз" +#: ml_model/exceptions.py:104 +#, python-format +msgid "Prompt is too long. Maximum length is %(max_length)s characters." +msgstr "Промпт слишком длинный. Максимальная длина — %(max_length)s символов." + #: ml_model/models.py:18 ml_model/models.py:38 ml_model/models.py:70 #: ml_model/models.py:182 tools/media/models.py:43 msgid "Slug" @@ -6,6 +6,7 @@ from asgiref.sync import async_to_sync from googletrans import Translator from messages.models import BaseStore, Message +from ml_model.exceptions import PromptLengthExceeded from ml_model.models import ( ModelCategory, ModelInput, @@ -56,6 +57,8 @@ class SimpleService(ABC): ) def translate_prompt(self, prompt: str, to: str = 'en'): + if len(prompt) > 3000: + raise PromptLengthExceeded return async_to_sync(self.translator.translate)(prompt, dest=to).text @abstractmethod @@ -141,11 +141,10 @@ class Chatgpt(SimpleService): chunks = self._get_file_data(file_extension, file_bytes) else: image = file - mime = kind.mime if kind else 'application/octet-stream' - normalized_image, image_size, image_data = self._get_image_data(mime, file_bytes, file_extension) + normalized_image, image_size, image_data = self._get_image_data(file_bytes, file_extension) input_content.append(image_data) except: - raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG']) + raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG', 'WEBP']) for proxy in Proxy.objects.all(): self.llm = ChatOpenAI( model=model_name, @@ -476,12 +475,12 @@ class Chatgpt(SimpleService): raise return raw_file_extension - def _get_image_data(self, mime: str, file_bytes: bytes, file_extension: str) -> Tuple: - normalized_image = Image.open(BytesIO(file_bytes)) - format = 'jpeg' if file_extension == 'jpg' else file_extension + def _get_image_data(self, file_bytes: bytes, file_extension: str) -> Tuple: + normalized_image = Image.open(BytesIO(file_bytes)).convert('RGB') buf = BytesIO() + format = 'jpeg' if file_extension not in ('png', 'jpeg', 'webp') else file_extension normalized_image.save(buf, format=format) - image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + image_url = f'data:image/{format};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}} @@ -113,10 +113,9 @@ class Chatgpt_5(Chatgpt): chunks = self._get_file_data(file_extension, file_bytes) else: image = file - mime = kind.mime if kind else 'application/octet-stream' - _, image_size, image_data = self._get_image_data(mime, file_bytes, file_extension) + _, image_size, image_data = self._get_image_data(file_bytes, file_extension) except Exception: - raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG']) + raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG', 'WEBP']) chat_history = self.get_chat_history(model_name=model_name) chat_history.add_message(HumanMessage(content=input_message.content)) llm_input = [SystemMessage(content=user_system_prompt), HumanMessage(content=input_content)] @@ -7,8 +7,11 @@ from io import BytesIO import filetype import requests from django.core.files import File -import logging + +from replicate.exceptions import ModelError + from messages.models import Message +from ml_model.exceptions import RequestBlocked, GenerationException from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -51,7 +54,12 @@ class Kling(SimpleService): input_message.file.close() callback_data.update({'start_image': image}) start_time = time.time() - video = replicate_run('kwaivgi/kling-v2.1', callback_data) + try: + video = replicate_run('kwaivgi/kling-v2.1', callback_data) + except ModelError as exc: + if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): + raise RequestBlocked + raise GenerationException from exc process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, mode=mode, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) @@ -94,3 +94,13 @@ class InvalidStyleCombinationError(Exception): class PredictionInterruptedError(Exception): def __str__(self): return _('Prediction interrupted. Please retry again') + + +class PromptLengthExceeded(Exception): + def __init__(self, max_length: int = 3000) -> None: + self.max_length = max_length + + def __str__(self) -> str: + return _('Prompt is too long. Maximum length is %(max_length)s characters.') % { + 'max_length': self.max_length + } @@ -26,6 +26,7 @@ from ml_model.exceptions import ( TemplateNotFound, TemplateUnknownException, RequestBlocked, + PromptLengthExceeded, ) from ml_model.services.base import SimpleService from payments.exceptions.insufficient_balance import InsufficientBalance @@ -163,7 +164,12 @@ class MessagesAPIView(APIView): {'detail': f'{exc}'}, status=HTTP_503_SERVICE_UNAVAILABLE, ) - except (FileExtensionNotSupported, ExceededContextLengthError, RequestBlocked) as exc: + except ( + FileExtensionNotSupported, + ExceededContextLengthError, + RequestBlocked, + PromptLengthExceeded, + ) as exc: return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) except TemplateNotFound as exc: return Response({'detail': f'{exc}'}, status=HTTP_500_INTERNAL_SERVER_ERROR) @@ -188,7 +194,10 @@ class MessagesAPIView(APIView): return Response(MessageSerializer(output_messages, many=True).data, 201) else: logger.info(serializer.errors) - return Response({'detail': '; '.join(serializer.errors['non_field_errors'])}, 400) + return Response( + {'detail': '; '.join([str(error) for error in sum(serializer.errors.values(), [])])}, + 400, + ) class MessageAPIView(APIView): @@ -15,7 +15,8 @@ from ml_model.exceptions import ( UnsupportedSize, FileNotProvided, ImageContentNotFound, - InvalidStyleCombinationError + InvalidStyleCombinationError, + PromptLengthExceeded, ) from ml_model.models import NeuronModel from ml_model.services.base import SimpleService @@ -158,9 +159,17 @@ class MediaAPIView(APIView): input_message.save() if isinstance(exc, InsufficientBalance): return Response({'detail': f'{exc}'}, status=HTTP_402_PAYMENT_REQUIRED) - if isinstance(exc, ( - UnsupportedSize, RequestBlocked, FileNotProvided, ImageContentNotFound, InvalidStyleCombinationError - )): + if isinstance( + exc, + ( + UnsupportedSize, + RequestBlocked, + FileNotProvided, + ImageContentNotFound, + InvalidStyleCombinationError, + PromptLengthExceeded, + ), + ): return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) return Response( { @@ -173,7 +182,10 @@ class MediaAPIView(APIView): return Response(MessageSerializer(output_messages, many=True).data, 201) else: logger.info(serializer.errors) - return Response({'detail': '; '.join(serializer.errors['non_field_errors'])}, 400) + return Response( + {'detail': '; '.join([str(error) for error in sum(serializer.errors.values(), [])])}, + 400, + ) class ModelImagesAPIView(MediaAPIView):