@@ -34,3 +34,9 @@ class UserAlreadyExists(Exception): class DomainNotFound(Exception): def __str__(self): return _('Domain not found') + + +class EmailSendFailed(Exception): + def __str__(self): + return _('Failed to send the email. Verify that the email exists and is available') + @@ -1,4 +1,5 @@ import logging +import smtplib from datetime import datetime from typing import Any, Sequence @@ -13,7 +14,7 @@ from django.utils.safestring import SafeString from django.utils.translation import gettext_lazy as _ from authentication.exceptions.email_exceptions import LetterNotFound, LetterUnknownException -from authentication.exceptions.user import DomainNotFound +from authentication.exceptions.user import DomainNotFound, EmailSendFailed from authentication.models import BusinessAccount, BusinessUserHost from authentication.models.user import CustomUserModel from authentication.services.email_token_service import EmailTokenService @@ -47,6 +48,10 @@ class EmailService: ) except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.Timeout): raise DomainNotFound + except smtplib.SMTPRecipientsRefused: + raise DomainNotFound + except smtplib.SMTPDataError: + raise EmailSendFailed except Exception as exc: logger.exception(exc) raise Exception(_('Error occured when proceed email sending')) @@ -46,6 +46,7 @@ from authentication.exceptions.email_token import EmailTokenNotFound from authentication.exceptions.user import ( DomainNotFound, EmailNotConfirmed, + EmailSendFailed, PasswordsDoNotMatch, UserAlreadyExists, WrongEmail, @@ -236,6 +237,8 @@ class UserAPIView(APIView): return Response({'detail': f'{exc}'}, status=status.HTTP_400_BAD_REQUEST) except DomainNotFound: return Response({'detail': _('Email not found')}, status=status.HTTP_400_BAD_REQUEST) + except EmailSendFailed as exc: + return Response({'detail': f'{exc}'}, status=status.HTTP_400_BAD_REQUEST) except Exception as exc: logger.exception(exc) return Response( @@ -315,7 +318,9 @@ class BusinessHostAPIView(APIView): except AdminCreateForbidden as err: return Response({'detail': str(err)}, status=status.HTTP_403_FORBIDDEN) except DomainNotFound: - return Response({'detail': _('Email sending error: email not found')}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {'detail': _('Email sending error: email not found')}, status=status.HTTP_400_BAD_REQUEST + ) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -361,13 +366,14 @@ class ReinviteBusinessAccountAPIView(APIView): """Reinvite business account including generation of a new password""" try: business_account = BusinessAccountSelector.filter_by_email( - email, - BusinessAccountService.get_company_name(request.user) + email, BusinessAccountService.get_company_name(request.user) ) BusinessHostService(request.user).reinvite_business_account(business_account=business_account) return Response({'detail': _('Business account has been reinvited')}, status=status.HTTP_200_OK) except DomainNotFound: - return Response({'detail': _('Email sending error: email not found')}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {'detail': _('Email sending error: email not found')}, status=status.HTTP_400_BAD_REQUEST + ) except LetterNotFound as exc: return Response({'detail': f'{exc}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except LetterUnknownException as exc: @@ -433,11 +439,15 @@ class ChangeBusinessAccountPassAPIView(APIView): BusinessAccountService.get_company_name(self.request.user), ) if not business_account: - return Response({'detail': _('Business account not found')}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {'detail': _('Business account not found')}, status=status.HTTP_400_BAD_REQUEST + ) BusinessAccountService(business_account).update_user_password( request.user, data['password_1'], data['password_2'] ) - return Response({'detail': _('Business account password has been updated')}, status=status.HTTP_200_OK) + return Response( + {'detail': _('Business account password has been updated')}, status=status.HTTP_200_OK + ) except (UnconfirmedUserChangePass, PasswordsDoNotMatch, AdminPasswordChangeForbidden) as exc: return Response({'detail': str(exc)}, status=status.HTTP_403_FORBIDDEN) except Exception as exc: @@ -647,7 +657,7 @@ class HostInvitationAPIView(APIView): request=AccountDataUpdateSerializer, responses={200: BusinessAccountDataSerializer}, ) - def put(self, request, user_email : str, *args, **kwargs): + def put(self, request, user_email: str, *args, **kwargs): """Update company sub-user.""" try: serializer = AccountDataUpdateSerializer(data=request.data) @@ -140,6 +140,10 @@ msgstr "Пользователь уже существует" msgid "Domain not found" msgstr "Домен не найден" +#: authentication/exceptions/user.py:41 +msgid "Failed to send the email. Verify that the email exists and is available" +msgstr "Не удалось отправить письмо. Убедитесь, что email существует и доступен" + #: authentication/models/business_account.py:18 payments/models/promocode.py:72 #: tools/public_api/models.py:34 tools/public_api/services/api_key.py:24 msgid "Owner" @@ -75,7 +75,7 @@ class Flux_2(SimpleService): height = input_message.info.pop('height', 1024) output_mp = math.ceil((width*height) / 1_000_000) callback_data = { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': input_message.content, 'aspect_ratio': 'custom', 'width': width, 'height': height, @@ -44,7 +44,7 @@ class Geminiimage(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: if input_message.content: callback_data = dict( - {'prompt': self.translate_prompt(input_message.content), **input_message.info} + {'prompt': input_message.content, **input_message.info} ) start_time = time.time() images = replicate_run('google/gemini-2.5-flash-image', callback_data) @@ -45,7 +45,7 @@ class Grok_Image(SimpleService): raise InsufficientBalance(balance, self.TOKENS_COST) callback_data = dict( { - 'prompt': f'{self.translate_prompt(input_message.content)}\n{self.OPTIMIZATION_PROMPT}', + 'prompt': f'{input_message.content}\n{self.OPTIMIZATION_PROMPT}', **input_message.info, } ) @@ -47,7 +47,7 @@ class Grok_Imagine_Video(SimpleService): cost := self.TOKENS_COST * duration ): raise InsufficientBalance(balance, cost) - callback_data = dict({'prompt': self.translate_prompt(input_message.content), **input_message.info}) + callback_data = dict({'prompt': input_message.content, **input_message.info}) if image := input_message.file: callback_data.update({'image': image.url}) start_time = time.time() @@ -3,9 +3,10 @@ from datetime import timedelta from decimal import Decimal from io import BytesIO from typing import Any -import filetype +import filetype import requests +from PIL import Image from django.core.files import File from messages.models import Message @@ -15,19 +16,39 @@ from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector - class Image_Test_Model(SimpleService): - TOKENS_COST = Decimal('3') - PLACEHOLDER_URL='https://i.pinimg.com/736x/8b/e0/61/8be06158da3986fb4c47497b5660bb29.jpg' + PLACEHOLDER_URL = 'https://i.pinimg.com/736x/8b/e0/61/8be06158da3986fb4c47497b5660bb29.jpg' + SUPPORTED_RATIOS = frozenset( + { + '1:1', + '16:9', + '9:16', + '4:5', + '5:4', + '3:4', + '4:3', + '2:3', + '3:2', + '9:21', + '20:9', + '21:9', + '1:2', + '2:1', + '19.5:9', + '9:20', + '9:19.5', + '7:4', + } + ) - def calculate_price(self, num_images: int = 1 ) -> Decimal: + def calculate_price(self, num_images: int = 1) -> Decimal: return self.TOKENS_COST * num_images @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: num_images = info.get('num_images', 1) - + return cls.TOKENS_COST * num_images def save_results( @@ -51,40 +72,69 @@ class Image_Test_Model(SimpleService): return Message.objects.bulk_create(messages) return messages - def make(self, input_message: Message, save: bool = True) -> list[Message]: num_images = input_message.info.get('num_images', 1) - - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < (cost := self.TOKENS_COST * num_images): + ratio = input_message.info.get('ratio', '1:1') + + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.TOKENS_COST * num_images + ): raise InsufficientBalance(balance, cost) - + start_time = time.time() ciu = input_message.info.get('ciu') or self.PLACEHOLDER_URL - images = [self._fetch_image(ciu)] * num_images - + raw_image = self._fetch_image(ciu) + resized_image = self._resize_to_ratio(raw_image, ratio) + images = [resized_image] * num_images + process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, num_images) - + msgs = self.save_results(input_message.content, process_time, images, save) return msgs - - - def _fetch_image(self, url: str): + def _fetch_image(self, url: str) -> bytes: try: response = requests.get(url, timeout=10) response.raise_for_status() except requests.RequestException: raise InvalidParameterError('Invalid image URL') - + kind = filetype.guess(response.content[:20]) - + if not kind: raise CorruptedFileError - + if not kind.mime.startswith('image/'): raise InvalidParameterError('Image format not supported') - + return response.content - \ No newline at end of file + + def _parse_ratio(self, ratio: str) -> tuple[float, float]: + if ratio not in self.SUPPORTED_RATIOS: + raise InvalidParameterError(f'Unsupported ratio: {ratio}') + rw, rh = map(float, ratio.split(':')) + return rw, rh + + def _resize_to_ratio(self, image_bytes: bytes, ratio: str) -> bytes: + rw, rh = self._parse_ratio(ratio) + + with Image.open(BytesIO(image_bytes)) as img: + img = img.convert('RGB') + src_w, src_h = img.size + target_ratio = rw / rh + src_ratio = src_w / src_h + + if src_ratio > target_ratio: + new_w = int(src_h * target_ratio) + left = (src_w - new_w) // 2 + cropped = img.crop((left, 0, left + new_w, src_h)) + else: + new_h = int(src_w / target_ratio) + top = (src_h - new_h) // 2 + cropped = img.crop((0, top, src_w, top + new_h)) + + buf = BytesIO() + cropped.save(buf, format='PNG') + return buf.getvalue() @@ -54,7 +54,7 @@ class Pixverse(SimpleService): raise InsufficientBalance(balance, cost) thinking_types = {'авто': 'auto', 'выкл.': 'disabled', 'вкл.': 'enabled'} callback_data = { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': input_message.content, 'quality': quality, 'thinking_type': thinking_types[input_message.info.pop('thinking_type', 'авто').lower()], **input_message.info, @@ -76,7 +76,7 @@ class Reve(SimpleService): raise InvalidParameterError(f'Unsupported size: {size}') callback_data = { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': input_message.content, **input_message.info, 'size': size, 'watermark': False, @@ -57,7 +57,7 @@ class Wan(SimpleService): input_message.file.close() callback_data = dict( { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': input_message.content, 'image': image, 'resolution': resolution, 'duration': duration, @@ -27,8 +27,7 @@ def _parse_model_slug(value: str, model_slugs: set[str]) -> str: close = difflib.get_close_matches(model_slug, model_slugs, n=1) if close: raise argparse.ArgumentTypeError( - f'NeuronModel со slug "{model_slug}" не найдена. ' - f'Возможно, вы имели в виду: {close[0]}' + f'NeuronModel со slug "{model_slug}" не найдена. Возможно, вы имели в виду: {close[0]}' ) raise argparse.ArgumentTypeError(f'NeuronModel со slug "{model_slug}" не найдена.') @@ -81,11 +80,14 @@ def _build_migration_source( return f"""# Generated by makemigration_payment_features on {datetime.now():%Y-%m-%d %H:%M} import math +import logging from decimal import Decimal from django.db import migrations from django.db.models import Max +logger = logging.getLogger(__name__) + def {func_name}(apps, schema_editor): PaymentPlan = apps.get_model('payments', 'PaymentPlan') @@ -95,7 +97,11 @@ def {func_name}(apps, schema_editor): price = Decimal('{price}') measurement_unit = '{measurement_unit}' price_threshold = {price_threshold} - model = NeuronModel.objects.get(slug='{model_slug}') + try: + model = NeuronModel.objects.get(slug='{model_slug}') + except NeuronModel.DoesNotExist: + logger.warning('The model %s was not found. Migration will do nothing.', '{model_slug}') + return category = model.category max_order_by_plan_id = {{ @@ -247,4 +253,3 @@ class Command(BaseCommand): return int(raw) except ValueError: self.stderr.write('Введите целое число.') - @@ -1,11 +1,14 @@ # Generated by makemigration_payment_features on 2026-07-28 15:33 import math +import logging from decimal import Decimal from django.db import migrations from django.db.models import Max +logger = logging.getLogger(__name__) + def add_flux_3_payment_features(apps, schema_editor): PaymentPlan = apps.get_model('payments', 'PaymentPlan') @@ -15,7 +18,11 @@ def add_flux_3_payment_features(apps, schema_editor): price = Decimal('9.5') measurement_unit = 'file' price_threshold = 0 - model = NeuronModel.objects.get(slug='flux_3') + try: + model = NeuronModel.objects.get(slug='flux_3') + except NeuronModel.DoesNotExist: + logger.warning('The model %s was not found. Migration will do nothing.', 'flux_3') + return category = model.category max_order_by_plan_id = { @@ -49,7 +56,6 @@ def add_flux_3_payment_features(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('payments', '0031_remove_paymentmethod_attempts_and_more'), ] @@ -1,11 +1,14 @@ # Generated by makemigration_payment_features on 2026-07-28 15:34 import math +import logging from decimal import Decimal from django.db import migrations from django.db.models import Max +logger = logging.getLogger(__name__) + def add_grok_image_ultra_payment_features(apps, schema_editor): PaymentPlan = apps.get_model('payments', 'PaymentPlan') @@ -15,7 +18,11 @@ def add_grok_image_ultra_payment_features(apps, schema_editor): price = Decimal('45') measurement_unit = 'file' price_threshold = 0 - model = NeuronModel.objects.get(slug='grok_image_ultra') + try: + model = NeuronModel.objects.get(slug='grok_image_ultra') + except NeuronModel.DoesNotExist: + logger.warning('The model %s was not found. Migration will do nothing.', 'grok_image_ultra') + return category = model.category max_order_by_plan_id = { @@ -49,7 +56,6 @@ def add_grok_image_ultra_payment_features(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('payments', '0032_add_flux_3_payment_features'), ] @@ -1,11 +1,14 @@ # Generated by makemigration_payment_features on 2026-07-28 17:27 +import logging import math from decimal import Decimal from django.db import migrations from django.db.models import Max +logger = logging.getLogger(__name__) + def add_flux_payment_features(apps, schema_editor): PaymentPlan = apps.get_model('payments', 'PaymentPlan') @@ -15,6 +18,7 @@ def add_flux_payment_features(apps, schema_editor): try: model = NeuronModel.objects.get(slug='flux') except NeuronModel.DoesNotExist: + logger.warning('The model %s was not found. Migration will do nothing.', 'flux') return price = Decimal('9.5') @@ -53,7 +57,6 @@ def add_flux_payment_features(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('payments', '0033_add_grok_image_ultra_payment_features'), ] @@ -15,7 +15,7 @@ HF_API_KEY=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 GOOGLE_API_KEY=AIzaSyBf9el4d_CY610zjCcesKxKL70BLfl57OM MISTRAL_API_KEY=CYtZSCQXZFzHcpJvWOjWNx4EHjf5kWQc DEEPL_API_KEY=4bb58b98-ca95-5978-9be0-ed437df6c15c:fx -SERPER_API_KEY=ed8e0dbcc26dacf3f7f99fbc8b3add9ada0c793e +SERPER_API_KEY=301101dad80f91df00bf4ff60a63c8a71922e2b7 FLUX_API_KEY=dccaf377-aecf-4cf0-aff4-dde47cee340d OPENROUTER_API_KEY=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 BYTEDANCE_MODEL_ARK_API_KEY=ark-151e9e89-7275-4dbf-bbb3-d2b32bb69d61-3ca5b