@@ -3,7 +3,7 @@ from authentication.exceptions.business_host_exceptions.already_account import ( ) from authentication.exceptions.business_host_exceptions.already_has_plan import ( AlreadyHasPlan, - InviteeHasPlan + InviteeHasPlan, ) from authentication.exceptions.business_host_exceptions.already_host import ( AlreadyHost, @@ -5,7 +5,7 @@ class AlreadyHasPlan(Exception): def __str__(self) -> str: return _( 'You already have an active tariff plan. You must request a ' - 'cancellation of your current tariff plan (via the \"Report an error\" button), after which ' + 'cancellation of your current tariff plan (via the "Report an error" button), after which ' 'you will be able to create a Corporate Account' ) @@ -14,6 +14,6 @@ class InviteeHasPlan(Exception): def __str__(self) -> str: return _( 'Invitee already has an active tariff plan. Invitee must request a ' - 'cancellation of his current tariff plan (via the \"Report an error\" button), after which ' + 'cancellation of his current tariff plan (via the "Report an error" button), after which ' 'you will be able to invite him' ) @@ -1,11 +1,4 @@ -from authentication.exceptions.email_exceptions.letter_not_found import ( - LetterNotFound -) -from authentication.exceptions.email_exceptions.letter_unknown import ( - LetterUnknownException -) +from authentication.exceptions.email_exceptions.letter_not_found import LetterNotFound +from authentication.exceptions.email_exceptions.letter_unknown import LetterUnknownException -__all__ = ( - 'LetterNotFound', - 'LetterUnknownException' -) \ No newline at end of file +__all__ = ('LetterNotFound', 'LetterUnknownException') @@ -39,4 +39,3 @@ class DomainNotFound(Exception): class EmailSendFailed(Exception): def __str__(self): return _('Failed to send the email. Verify that the email exists and is available') - @@ -16,8 +16,6 @@ class EmailToken(BaseModel): key = models.CharField(max_length=30, verbose_name=_('Key')) class Meta: - indexes = [ - Index(fields=['key'], name='idx_email_token_key') - ] + indexes = [Index(fields=['key'], name='idx_email_token_key')] verbose_name = _('Email Token') verbose_name_plural = _('Email Tokens') @@ -19,4 +19,6 @@ class AccountStatusSelector: return False def is_admin(self) -> bool: - return self.user.business_account.account_privileges == 'admin' if self.is_business_account() else False \ No newline at end of file + return ( + self.user.business_account.account_privileges == 'admin' if self.is_business_account() else False + ) @@ -83,7 +83,8 @@ class BusinessHostSelector: else: raise Exception(_("User haven't rights to access host account information")) return BusinessHostSerializer( - host, context={'worker_amount': host.accounts.count(), 'token_cap_enabled': host.token_cap_enabled} + host, + context={'worker_amount': host.accounts.count(), 'token_cap_enabled': host.token_cap_enabled}, ) def get_per_model_statistics(self): @@ -9,7 +9,6 @@ from rest_framework_simplejwt.tokens import RefreshToken from social_django.models import UserSocialAuth from authentication.models import CustomUserModel -from authentication.models.choices import InvitationStatus from authentication.models.user_telegram import TelegramUser from authentication.models.user_vk import VKUser from authentication.selectors.account_status_selector import ( @@ -4,7 +4,6 @@ from django.utils.translation import gettext_lazy as _ from authentication.exceptions.business_account import ( AdminPasswordChangeForbidden, - PasswordChangeRestricted, UnconfirmedUserChangePass, ) from authentication.exceptions.user import PasswordsDoNotMatch @@ -38,7 +38,6 @@ from authentication.serializers import ( BusinessAccountDataSerializer, BusinessHostSerializer, BusinessHostUpdateSerializer, - DeleteBusinessAccountSerializer, DeleteModelsSerializer, NewBusinessAccountSerializer, NewBusinessHostSerializer, @@ -72,7 +71,9 @@ class BusinessHostService: EmailService.send_corporate_greeting_email(account_service.account, password) return account_service - def create_existing(self, email: str, account_privileges: str, group: UUID | None = None) -> BusinessAccountService: + def create_existing( + self, email: str, account_privileges: str, group: UUID | None = None + ) -> BusinessAccountService: company = self.user.host or self.user.employee.parent_company if self.user.account_type == 'business_admin' and account_privileges == 'admin': raise AdminCreateForbidden @@ -91,7 +92,9 @@ class BusinessHostService: company, account_privileges=account_privileges, ) - account_service.account.group = BusinessGroup.objects.filter(uid=group, parent_company=company).first() + account_service.account.group = BusinessGroup.objects.filter( + uid=group, parent_company=company + ).first() account_service.account.save() if not user.is_deleted: EmailService.send_corporate_invitation_email(account_service.account, company) @@ -19,9 +19,7 @@ class MeAPITest(BaseAuthorizedAPITest): @classmethod def setup_test_data(cls) -> None: - cls.payment_plan, _ = PaymentPlan.objects.update_or_create( - price=0, tokens_per_plan=10, defaults={} - ) + cls.payment_plan, _ = PaymentPlan.objects.update_or_create(price=0, tokens_per_plan=10, defaults={}) cls.setup_host() def test_unauthorized_status_code(self) -> None: @@ -91,4 +89,3 @@ class MeAPITest(BaseAuthorizedAPITest): BusinessAccount.objects.create(user=self.user, parent_company=self.host) payment_plan_uid = self.get().json()['payment_plan']['plan']['uid'] self.assertEqual(str(payment_plan_uid), str(self.host_payment_plan.uid)) - @@ -3,9 +3,6 @@ from rest_framework.request import Request from rest_framework.views import APIView from authentication.models.choices import AccountPrivileges -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) from backend import settings @@ -9,12 +9,11 @@ from django.dispatch import receiver @receiver([post_save, post_delete], sender=BusinessAccount) def invalidate_user_cache(sender, instance, signal, **kwargs): - cache_keys = cache.conn.smembers(dnfs_to_conj_keys( - '', - {'authentication_customusermodel': [{'uid': instance.user_id}]} - )[0]) + cache_keys = cache.conn.smembers( + dnfs_to_conj_keys('', {'authentication_customusermodel': [{'uid': instance.user_id}]})[0] + ) for key in cache_keys: data = cache.get(key.decode()) if isinstance(data, list) and isinstance((user := data[0]), CustomUserModel): user.business_account = instance if signal == post_save else None - cache.set(key.decode(), [user]) \ No newline at end of file + cache.set(key.decode(), [user]) @@ -1,5 +1,4 @@ from django.urls import include, path -from rest_framework_simplejwt.views import TokenRefreshView from authentication import views @@ -8,8 +8,8 @@ app = Celery('backend') app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() +from lib.unleash.client import celery_client # noqa: E402 -from lib.unleash.client import celery_client @worker_process_init.connect def configure_workers(sender=None, conf=None, **kwargs): @@ -491,7 +491,9 @@ UNLEASH_INSTANCE_ID = env.str('UNLEASH_INSTANCE_ID', '') UNLEASH_WEBHOOK_SECRET_KEY = env.str('UNLEASH_WEBHOOK_SECRET_KEY', 'defaultsecretkey') # RECURRING SETTINGS -RECURRING_RETRY_OFFSETS = env.list('RECURRING_RETRY_OFFSETS', default=[1, 3, 5, 8, 12, 16, 21, 28], subcast=int) +RECURRING_RETRY_OFFSETS = env.list( + 'RECURRING_RETRY_OFFSETS', default=[1, 3, 5, 8, 12, 16, 21, 28], subcast=int +) RECURRING_FULL_ACCESS_CUTOFF_DAY = env.int('RECURRING_FULL_ACCESS_CUTOFF_DAY', 4) RECURRING_FAILED_CHARGE_EMAIL_DAYS = env.list('RECURRING_FAILED_CHARGE_EMAIL_DAYS', default=[3], subcast=int) @@ -138,4 +138,4 @@ urlpatterns += ( ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + public_urlpatterns -) \ No newline at end of file +) @@ -103,5 +103,3 @@ class BaseAuthorizedAPITest(BaseAPITest): @abstractmethod def test_authorized_status_code(self) -> None: ... - - @@ -5,7 +5,6 @@ from lib.typing import Email, State class FeatureFlagService(ABC): - @abstractmethod def get_flag_state_by_emails(self, name: str, emails: List[Email]) -> Mapping[Email, State]: pass @@ -23,5 +23,5 @@ class UnleashRedisCache(BaseCache): def destroy(self): client = self.cache.client.get_client(write=True) - for key in client.scan_iter(f"{self.PREFIX}*"): + for key in client.scan_iter(f'{self.PREFIX}*'): client.delete(key) @@ -1,4 +1,4 @@ from lib.services.unleash_feature_flag import UnleashFeatureFlagService web_client = UnleashFeatureFlagService() -celery_client = UnleashFeatureFlagService() \ No newline at end of file +celery_client = UnleashFeatureFlagService() @@ -1,2 +1,2 @@ type Email = str -type State = bool \ No newline at end of file +type State = bool @@ -17,11 +17,11 @@ class MessageAdmin(admin.ModelAdmin): 'file', 'from_public_api', 'is_sent', - 'info' + 'info', ) raw_id_fields = ('content_type',) - @admin.display(description="Связанный объект") + @admin.display(description='Связанный объект') def content_object_link(self, obj): if c_obj := obj.content_object: url = reverse(f'admin:{c_obj._meta.app_label}_{c_obj._meta.model_name}_change', args=[c_obj.pk]) @@ -41,7 +41,9 @@ class MessageSerializer(serializers.ModelSerializer): file = data.get('file') max_mb_size = 50 if file and file.size > (max_mb_size << 10 << 10): - raise ValidationError(_('The file size cannot exceed %(max_mb_size)d MB') % {'max_mb_size': max_mb_size}) + raise ValidationError( + _('The file size cannot exceed %(max_mb_size)d MB') % {'max_mb_size': max_mb_size} + ) return data def to_representation(self, instance): @@ -1 +1 @@ -from ml_model.adapters.bytedance_model_ark import BytedanceModelArkAdapter \ No newline at end of file +from ml_model.adapters.bytedance_model_ark import BytedanceModelArkAdapter @@ -122,7 +122,7 @@ class BytedanceModelArkAdapter: raise PromptLengthExceeded if 'Input length' in resp.text and 'exceeds the maximum length' in resp.text: - match = re.search(r"Input length (\d+) exceeds the maximum length (\d+)", resp.text) + match = re.search(r'Input length (\d+) exceeds the maximum length (\d+)', resp.text) if match: max_length = int(match.group(2)) raise PromptLengthExceeded(max_length=max_length) @@ -319,9 +319,7 @@ class BytedanceModelArkAdapter: chunk = delta.get('content') or '' if chunk: yield RawSSEChunk(event='token', data={'content': chunk}) - if ( - fr := choices[0].get('finish_reason') - ) and fr not in ( + if (fr := choices[0].get('finish_reason')) and fr not in ( BytedanceFinishReason.STOP, BytedanceFinishReason.LENGTH, ): @@ -381,7 +379,9 @@ class BytedanceModelArkAdapter: raise InputImageSensitiveContentError if image_data := data.get('data'): - urls = [item.get('url') for item in image_data if isinstance(item, dict) and item.get('url')] + urls = [ + item.get('url') for item in image_data if isinstance(item, dict) and item.get('url') + ] if urls: return urls if cls._is_request_blocked_error(data): @@ -507,7 +507,7 @@ class BytedanceModelArkAdapter: try: response = client.post('tokenization', json={'model': model, 'text': [text]}).json() return response['data'][0]['total_tokens'] - except: + except Exception: return 0 @classmethod @@ -529,5 +529,5 @@ class BytedanceModelArkAdapter: for token_info in data: total_tokens += token_info['total_tokens'] return total_tokens - except: - return 0 \ No newline at end of file + except Exception: + return 0 @@ -72,7 +72,9 @@ class OpenrouterAdapter: data_obj = json.loads(data) if data_obj.get('choices'): content_chunk = data_obj['choices'][0].get('delta', {}).get('content') or '' - reasoning_chunk = data_obj['choices'][0].get('delta', {}).get('reasoning') or '' + reasoning_chunk = ( + data_obj['choices'][0].get('delta', {}).get('reasoning') or '' + ) if content_chunk: content += content_chunk yield RawSSEChunk(event='token', data={'content': content_chunk}) @@ -3,7 +3,6 @@ from uuid import UUID from django.db.models import Prefetch, Q, Exists, OuterRef from django.utils.translation import gettext_lazy as _ -from authentication.models.choices import InvitationStatus from authentication.models.user import CustomUserModel from authentication.selectors.user_selector import UserSelector from ml_model.exceptions import NeuronModelNotExist @@ -38,7 +37,7 @@ class NeuronModelSelector: 'audio': 'audio', 'video': 'videos', 'code': 'code', - 'voice': 'voice' + 'voice': 'voice', } models = NeuronModel.objects.prefetch_related( Prefetch( @@ -57,13 +56,21 @@ class NeuronModelSelector: hidden: bool = False, ): if self.user.is_anonymous: - models = NeuronModel.objects.prefetch_related(Prefetch('model_modelstats')).filter(model_settings__is_active=True) + models = NeuronModel.objects.prefetch_related(Prefetch('model_modelstats')).filter( + model_settings__is_active=True + ) return NeuronModelsSerializer(models, many=True) user_type = UserSelector(self.user).check_account_type() models = NeuronModel.objects.annotate( - has_chat_msgs=Exists(Chat.objects.filter(user=self.user, model=OuterRef('uid'), messages__isnull=False)), - has_image_msgs=Exists(Image.objects.filter(user=self.user, model=OuterRef('uid'), messages__isnull=False)), - has_video_msgs=Exists(Video.objects.filter(user=self.user, model=OuterRef('uid'), messages__isnull=False)), + has_chat_msgs=Exists( + Chat.objects.filter(user=self.user, model=OuterRef('uid'), messages__isnull=False) + ), + has_image_msgs=Exists( + Image.objects.filter(user=self.user, model=OuterRef('uid'), messages__isnull=False) + ), + has_video_msgs=Exists( + Video.objects.filter(user=self.user, model=OuterRef('uid'), messages__isnull=False) + ), ).filter( ( Q(private_models_hosts__isnull=True) @@ -72,17 +79,10 @@ class NeuronModelSelector: ) & ( Q(model_settings__is_active=True) - | ( - Q(has_chat_msgs=True) - | Q(has_image_msgs=True) - | Q(has_video_msgs=True) - ) + | (Q(has_chat_msgs=True) | Q(has_image_msgs=True) | Q(has_video_msgs=True)) ) ) - if ( - user_type == 'business_account' - and self.user.employee.accepted - ): + if user_type == 'business_account' and self.user.employee.accepted: allowed_models = self.user.employee.parent_company.allowed_models else: allowed_models = None @@ -64,7 +64,7 @@ class FileProcessingService: raw_text += content fitz.TOOLS.store_shrink(100) except Exception: - return f'Ошибка: Файл поврежден или не может быть прочитан.' + return 'Ошибка: Файл поврежден или не может быть прочитан.' return f'Содержимое файла: {raw_text.strip()}' @classmethod @@ -16,14 +16,15 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector import random + class Audio_Test_Model(SimpleService): - TOKENS_COST = Decimal('3') - - PLACEHOLDER_URL=[ + + PLACEHOLDER_URL = [ 'https://www.myinstants.com/media/sounds/saliut-eblany-batia-doma-billy-butcher-i-the-boys.mp3', 'https://www.myinstants.com/media/sounds/zdravstvuite-nichtozhnye-nishchie-smertnye.mp3', - 'https://www.myinstants.com/media/sounds/okh-zria-ia-tuda-polez.mp3'] # Позже убрать + 'https://www.myinstants.com/media/sounds/okh-zria-ia-tuda-polez.mp3', + ] # Позже убрать def calculate_price(self, num_audios: int = 1) -> Decimal: return self.TOKENS_COST * num_audios @@ -31,7 +32,7 @@ class Audio_Test_Model(SimpleService): @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: num_audios = info.get('num_audios', 1) - + return cls.TOKENS_COST * num_audios def save_results( @@ -55,43 +56,40 @@ class Audio_Test_Model(SimpleService): return Message.objects.bulk_create(messages) return messages - def make(self, input_message: Message, save: bool = True) -> list[Message]: - cau = input_message.info.get('cau') or self.PLACEHOLDER_URL[random.randint(0, 2)] # Позже убрать + cau = input_message.info.get('cau') or self.PLACEHOLDER_URL[random.randint(0, 2)] # Позже убрать num_audios = input_message.info.get('num_audios', 1) - - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < (cost := self.calculate_price(num_audios)): + + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.calculate_price(num_audios) + ): raise InsufficientBalance(balance, cost) - + start_time = time.time() - + audio_bytes = self._fetch_audio(cau) - + audios = [audio_bytes] * num_audios - + process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, num_audios) - + msgs = self.save_results(input_message.content, process_time, audios, save) return msgs - def _fetch_audio(self, url: str): try: - response = requests.get( - url, - timeout=600 - ) + response = requests.get(url, timeout=600) response.raise_for_status() except requests.RequestException as exc: raise InvalidParameterError(f'Invalid audio URL: {exc}') - + kind = filetype.guess(response.content[:120]) - + if not kind: raise CorruptedFileError - + if not kind.mime.startswith('audio/'): raise InvalidParameterError('Audio format not supported') - - return response.content \ No newline at end of file + + return response.content @@ -6,7 +6,6 @@ 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, @@ -275,7 +275,9 @@ class Chatgpt(Chatgpt_4, StreamSimpleService, OpenAIStreamMixin): try: for proxy in Proxy.objects.all(): - json_data, predicted_input_tokens = self._build_payload(proxy, input_message, ctx, include_image_tool=False) + json_data, predicted_input_tokens = self._build_payload( + proxy, input_message, ctx, include_image_tool=False + ) model_name = ctx['model_name'] self.logger.info( f'Predicted input tokens (responses/input_tokens) для {model_name} - {predicted_input_tokens}' @@ -321,12 +323,7 @@ class Chatgpt(Chatgpt_4, StreamSimpleService, OpenAIStreamMixin): return result def _build_payload( - self, - proxy: Proxy, - input_message: Message, - ctx: dict[str, Any], - *, - include_image_tool: bool = True + self, proxy: Proxy, input_message: Message, ctx: dict[str, Any], *, include_image_tool: bool = True ) -> tuple[dict[str, Any], int]: if not ctx: info = input_message.info.copy() @@ -9,7 +9,6 @@ from math import ceil import time from typing import Any, Dict, Generator, List, Optional, Tuple -from django.utils.translation import gettext_lazy as _ import filetype import httpx from langchain.chains import ConversationChain @@ -35,7 +34,7 @@ from ml_model.exceptions import ( FileUploadUnsupported, ModelVersionNotAvailable, ) -from ml_model.models import ModelConfiguration, NeuronModel +from ml_model.models import ModelConfiguration from ml_model.services.EmbeddingService import EmbeddingService from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService @@ -12,7 +12,6 @@ from ml_model.exceptions import ( FileExtensionNotSupported, InvalidParameterError, ) -from ml_model.models import NeuronModel from ml_model.services.chatgpt_4 import Chatgpt_4 from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.EmbeddingService import EmbeddingService @@ -92,7 +91,6 @@ class Chatgpt_5(Chatgpt_4): TOKEN_LIMITS = {key: 200_000 for key in TOKENS_COST.keys()} - def make( self, input_message: Message, @@ -32,6 +32,7 @@ from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore + class Claude(SerperMixin, StreamSimpleService): """ Claude Service @@ -284,10 +285,7 @@ class Claude(SerperMixin, StreamSimpleService): if (chunks_length := sum(len(chunk) for chunk in chunks)) > 20_000: predict_embedding_tokens = len(chunks) * 2020 predicted_input_price += ( - ( - Decimal('210') - + Decimal(chunks_length) / Decimal(len(chunks)) * Decimal('10') - ) + (Decimal('210') + Decimal(chunks_length) / Decimal(len(chunks)) * Decimal('10')) / Decimal('2.7') * self.TOKENS_COST[version_slug]['input'] / Decimal('1_000_000') @@ -354,14 +352,10 @@ class Claude(SerperMixin, StreamSimpleService): } reasoning_effort = callback_data['reasoning']['effort'] reasoning_input_tokens = ( - {'low': 150, 'medium': 250}.get(reasoning_effort, 0) - if version_slug == 'claude-fable-5' - else 0 + {'low': 150, 'medium': 250}.get(reasoning_effort, 0) if version_slug == 'claude-fable-5' else 0 ) reasoning_output_reserve = ( - {'low': 300, 'medium': 500}.get(reasoning_effort, 0) - if version_slug == 'claude-fable-5' - else 0 + {'low': 300, 'medium': 500}.get(reasoning_effort, 0) if version_slug == 'claude-fable-5' else 0 ) estimated_input_tokens = ( Decimal( @@ -375,9 +369,7 @@ class Claude(SerperMixin, StreamSimpleService): + reasoning_input_tokens ) predicted_input_price += ( - estimated_input_tokens - * self.TOKENS_COST[version_slug]['input'] - / Decimal('1_000_000') + estimated_input_tokens * self.TOKENS_COST[version_slug]['input'] / Decimal('1_000_000') + predict_embedding_tokens * self.TOOLS_TOKEN_COSTS['text-embedding-3-small']['output'] ) @@ -8,7 +8,6 @@ import requests from django.core.files import File 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 @@ -67,7 +67,7 @@ class Deepseek(SimpleService): messages = [ {'role': 'system', 'content': system_prompt}, *self.get_chat_history(), - {'role': 'user', 'content': input_message.content} + {'role': 'user', 'content': input_message.content}, ] start_time = time.time() @@ -83,15 +83,15 @@ class Deepseek(SimpleService): return msgs - - - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[dict[str, str | list]]: + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[1:message_limit + 1] + ).order_by('-created_at')[1 : message_limit + 1] ) ) elif isinstance(self.store, APIStore): @@ -117,4 +117,3 @@ class Deepseek(SimpleService): while character_length > max_character_limit: character_length -= len(memory.pop(0)['content']) return memory - @@ -39,7 +39,7 @@ class Dola_Seed(SimpleService): 'long_prompt': { 'input': Decimal('500'), 'output': Decimal('3000'), - } + }, }, 'seed-2-0-mini': { 'short_prompt': { @@ -49,7 +49,7 @@ class Dola_Seed(SimpleService): 'long_prompt': { 'input': Decimal('100'), 'output': Decimal('400'), - } + }, }, # 1M tokens } @@ -124,11 +124,7 @@ class Dola_Seed(SimpleService): raise GenerationException data = json.loads(result.stdout) video_stream = next( - ( - stream - for stream in data.get('streams', []) - if stream.get('codec_type') == 'video' - ), + (stream for stream in data.get('streams', []) if stream.get('codec_type') == 'video'), None, ) if not video_stream: @@ -136,9 +132,7 @@ class Dola_Seed(SimpleService): duration = float(data.get('format', {}).get('duration') or 0) if duration <= 0: raise GenerationException - fps = cls._parse_video_fps( - video_stream.get('r_frame_rate') or video_stream.get('avg_frame_rate') - ) + fps = cls._parse_video_fps(video_stream.get('r_frame_rate') or video_stream.get('avg_frame_rate')) return int(video_stream['width']), int(video_stream['height']), duration, fps @staticmethod @@ -158,7 +152,7 @@ class Dola_Seed(SimpleService): def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: price_map = self.TOKENS_COST[version] - prompt_type = "short_prompt" if input_tokens <= 128_000 else "long_prompt" + prompt_type = 'short_prompt' if input_tokens <= 128_000 else 'long_prompt' price = ( input_tokens * price_map[prompt_type]['input'] / 1_000_000 + output_tokens * price_map[prompt_type]['output'] / 1_000_000 @@ -179,9 +173,7 @@ class Dola_Seed(SimpleService): return msgs - def _prepare_data( - self, input_message: Message - ) -> tuple[str, dict[str, Any], list[dict[str, Any]]]: + def _prepare_data(self, input_message: Message) -> tuple[str, dict[str, Any], list[dict[str, Any]]]: info = input_message.info.copy() version = info.pop('version', None) if version is None or version not in self.TOKENS_COST: @@ -207,9 +199,7 @@ class Dola_Seed(SimpleService): with Image.open(BytesIO(file_bytes)) as normalized_image: image_width, image_height = normalized_image.size elif attachment_type == 'video_url': - video_width, video_height, video_duration, video_fps = self._get_video_metadata( - file_bytes - ) + video_width, video_height, video_duration, video_fps = self._get_video_metadata(file_bytes) content.append({'type': attachment_type, attachment_type: {'url': file.url}}) messages.append({'role': 'user', 'content': content}) api_model = self.VERSION_MAPPING[version] @@ -221,9 +211,7 @@ class Dola_Seed(SimpleService): else: texts.append( ''.join( - str(item.get('text') or '') - for item in message_content - if isinstance(item, dict) + str(item.get('text') or '') for item in message_content if isinstance(item, dict) ) ) predicted_input_tokens = BytedanceModelArkAdapter.batch_tokenize(api_model, texts) @@ -274,7 +262,7 @@ class Dola_Seed(SimpleService): callback_data=callback_data, content_type=BytedanceContentType.CHAT, messages=messages, - include_reasoning=True + include_reasoning=True, ) process_time = timedelta(seconds=(time.time() - start_time)) @@ -327,13 +315,9 @@ class Dola_Seed(SimpleService): input_text_parts.append(content) else: input_text_parts.extend( - str(item.get('text') or '') - for item in content - if isinstance(item, dict) + str(item.get('text') or '') for item in content if isinstance(item, dict) ) - input_tokens = BytedanceModelArkAdapter.tokenize( - model, ''.join(input_text_parts) - ) + input_tokens = BytedanceModelArkAdapter.tokenize(model, ''.join(input_text_parts)) output_tokens = BytedanceModelArkAdapter.tokenize(model, result) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( @@ -355,7 +339,7 @@ class Dola_Seed(SimpleService): reversed( Message.objects.filter( chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[1:message_limit + 1] + ).order_by('-created_at')[1 : message_limit + 1] ) ) elif isinstance(self.store, APIStore): @@ -72,10 +72,7 @@ class Elevenlabs(SimpleService): raw_file_extension = kind.extension file_extension = file_service.get_file_extension(raw_file_extension, file_bytes) if file_extension in ('pdf', 'doc', 'docx'): - raw_text = ( - file_service.get_file_data(file_extension, file_bytes) - .replace('\n', ' ') - ) + raw_text = file_service.get_file_data(file_extension, file_bytes).replace('\n', ' ') else: raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX']) max_affordable_chars = max( @@ -9,7 +9,7 @@ from django.core.files import File from django.utils.translation import gettext as _ from messages.models import Message -from ml_model.exceptions import RequestBlocked, GenerationException, InvalidParameterError +from ml_model.exceptions import InvalidParameterError from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -12,8 +12,9 @@ from django.core.files import File from django.core.files.images import get_image_dimensions from messages.models import Message -from ml_model.exceptions import PredictionInterruptedError, RequestBlocked, GenerationException, \ - FileExtensionNotSupported +from ml_model.exceptions import ( + FileExtensionNotSupported, +) from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -39,9 +40,9 @@ class Flux_2(SimpleService): def calculate_price(self, version: str, input_mp: int, output_mp: int) -> Decimal: version_price = self.TOKENS_COST[version] price = ( - version_price.get('run', Decimal('0')) + - version_price['input_mp'] * input_mp + - version_price['output_mp'] * output_mp + version_price.get('run', Decimal('0')) + + version_price['input_mp'] * input_mp + + version_price['output_mp'] * output_mp ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -73,7 +74,7 @@ class Flux_2(SimpleService): raise ModelVersionNotAvailable(version, self.TOKENS_COST) width = input_message.info.pop('width', 1024) height = input_message.info.pop('height', 1024) - output_mp = math.ceil((width*height) / 1_000_000) + output_mp = math.ceil((width * height) / 1_000_000) callback_data = { 'prompt': input_message.content, 'aspect_ratio': 'custom', @@ -95,12 +96,14 @@ class Flux_2(SimpleService): with BytesIO() as buf: normalized_image.save(buf, format=format) file_width, file_height = get_image_dimensions(buf) - input_mp = math.ceil((file_width*file_height) / 1_000_000) + input_mp = math.ceil((file_width * file_height) / 1_000_000) image = f'data:image/{format};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' normalized_image.close() callback_data.update({'input_images': [image]}) images = [replicate_run(f'black-forest-labs/{version}', callback_data)] process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, version=version, input_mp=input_mp, output_mp=output_mp) + self.handle_invoice( + input_message.content_object.model, version=version, input_mp=input_mp, output_mp=output_mp + ) msgs = self.save_results(input_message.content, images, process_time, save) return msgs @@ -8,8 +8,6 @@ from django.core.files import File 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 @@ -65,7 +63,7 @@ class Fluxpulid(SimpleService): translated_prompt = self.translate_prompt(input_message.content) callback_data = dict( { - 'prompt': f"{translated_prompt}\n{self.OPTIMIZATION_PROMPT}", + 'prompt': f'{translated_prompt}\n{self.OPTIMIZATION_PROMPT}', 'main_face_image': BytesIO(input_message.file.read()), 'output_quality': 100, 'output_format': 'png', @@ -24,7 +24,6 @@ from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore - class Gemini_3_1(StreamSimpleService): TOKENS_COST = { 'gemini-3.1-pro-preview': { @@ -36,14 +35,16 @@ class Gemini_3_1(StreamSimpleService): 'input': Decimal('75'), 'output': Decimal('450'), 'highest_prices': {'input': Decimal('75'), 'output': Decimal('450')}, - } + }, } TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} SUPPORTED_EXTENSIONS = ['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG', 'WEBP'] - def calculate_price(self, version: str, input_tokens: int, output_tokens: int, embedding_tokens: int) -> Decimal: + def calculate_price( + self, version: str, input_tokens: int, output_tokens: int, embedding_tokens: int + ) -> Decimal: price_map = self.TOKENS_COST[version] if input_tokens >= 200_000 or output_tokens >= 200_000: price = ( @@ -177,9 +178,7 @@ class Gemini_3_1(StreamSimpleService): character_length -= len(memory.pop(0)['content']) return memory - def _prepare_messages( - self, input_message: Message - ) -> tuple[list[dict[str, str | list]], int]: + def _prepare_messages(self, input_message: Message) -> tuple[list[dict[str, str | list]], int]: messages = self.get_chat_history() messages.insert( 0, @@ -8,7 +8,7 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import ModelTimeoutError, ImageContentNotFound, RequestBlocked, GenerationException +from ml_model.exceptions import ModelTimeoutError from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -43,9 +43,7 @@ class Geminiimage(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: if input_message.content: - callback_data = dict( - {'prompt': input_message.content, **input_message.info} - ) + callback_data = dict({'prompt': input_message.content, **input_message.info}) start_time = time.time() images = replicate_run('google/gemini-2.5-flash-image', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) @@ -129,7 +129,7 @@ class Gptimage(SimpleService): for proxy in Proxy.objects.all(): moderation = 'low' if self.store.user.account_type == 'regular' else 'auto' json_data = { - 'prompt': f"{input_message.content}\n{self.OPTIMIZATION_PROMPT}", + 'prompt': f'{input_message.content}\n{self.OPTIMIZATION_PROMPT}', 'model': 'gpt-image-2', 'n': 1, 'quality': quality, @@ -1,5 +1,4 @@ import base64 -import logging import time from datetime import timedelta from decimal import Decimal @@ -31,7 +30,6 @@ class Grok_4_1_Fast(SimpleService): MAX_PIXELS = 178956970 - def calculate_price(self, input_tokens: int, output_tokens: int, embedding_tokens: int) -> Decimal: price = ( input_tokens * self.TOKENS_COST['input'] / 1_000_000 @@ -115,10 +113,7 @@ class Grok_4_1_Fast(SimpleService): raise ImageTooLargeError(self.MAX_PIXELS) mime = kind.mime if kind else 'application/octet-stream' - image_url = ( - f'data:{mime};base64,' - f'{base64.b64encode(file_bytes).decode("utf-8")}' - ) + image_url = f'data:{mime};base64,{base64.b64encode(file_bytes).decode("utf-8")}' messages[-1]['content'] = [ {'type': 'text', 'text': input_message.content}, @@ -1,16 +1,14 @@ -import base64 import time from datetime import timedelta from decimal import Decimal from io import BytesIO from typing import Any -import filetype import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import GenerationException, PromptLengthExceeded, RequestBlocked +from ml_model.exceptions import PromptLengthExceeded from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -10,7 +10,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import RequestBlocked, GenerationException from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -21,14 +20,8 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Hailuo(SimpleService): TOKENS_COST = { - 'hailuo-2.3': { - '768p': Decimal('84'), - '1080p': Decimal('147') - }, - 'hailuo-2.3-fast': { - '768p': Decimal('57'), - '1080p': Decimal('99') - } + 'hailuo-2.3': {'768p': Decimal('84'), '1080p': Decimal('147')}, + 'hailuo-2.3-fast': {'768p': Decimal('57'), '1080p': Decimal('99')}, } def calculate_price(self, version: str, resolution: str) -> Decimal: @@ -58,12 +51,15 @@ class Hailuo(SimpleService): if version is None or version not in self.TOKENS_COST: raise ModelVersionNotAvailable(version, self.TOKENS_COST) resolution = input_message.info.get('resolution', '768p') - if ( - (balance := PaymentPlanSelector(self.store.user).get_current_balance()) - < (cost := self.TOKENS_COST[version][resolution]) + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.TOKENS_COST[version][resolution] ): raise InsufficientBalance(balance, cost) - callback_data = {'prompt': self.translate_prompt(input_message.content), 'duration': 6, **input_message.info} + callback_data = { + 'prompt': self.translate_prompt(input_message.content), + 'duration': 6, + **input_message.info, + } if input_message.file: kind = filetype.guess(input_message.file.read(20)) mime = kind.mime if kind else 'application/octet-stream' @@ -72,10 +68,7 @@ class Hailuo(SimpleService): input_message.file.close() callback_data.update({'first_frame_image': image}) start_time = time.time() - video = replicate_run( - f'minimax/{version}', - callback_data - ) + video = replicate_run(f'minimax/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version, resolution=resolution) msgs = self.save_results(input_message.content, process_time, video, save) @@ -8,7 +8,7 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import InvalidStyleCombinationError, RequestBlocked, GenerationException +from ml_model.exceptions import InvalidStyleCombinationError from ml_model.models import ( NeuronModel, ) @@ -68,10 +68,9 @@ class Ideogram(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() version = 'ideogram-v3-turbo' - if ( - input_message.info.get('style_preset', 'None') != 'None' - and input_message.info.get('style_type', 'None') not in ('None', 'Auto', 'General') - ): + if input_message.info.get('style_preset', 'None') != 'None' and input_message.info.get( + 'style_type', 'None' + ) not in ('None', 'Auto', 'General'): raise InvalidStyleCombinationError callback_data = dict( { @@ -8,7 +8,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import ImageContentNotFound, GenerationException, RequestBlocked from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run from payments.exceptions.insufficient_balance import InsufficientBalance @@ -26,23 +26,20 @@ class Llama(SimpleService): """ TOKENS_COST = { - 'llama-3.3-70b-instruct': { - 'input': Decimal('84'), - 'output': Decimal('84') - }, # 1M tokens + 'llama-3.3-70b-instruct': {'input': Decimal('84'), 'output': Decimal('84')}, # 1M tokens 'llama-4-maverick': { 'input': Decimal('180'), 'output': Decimal('180'), - 'input_imgs': Decimal('200.52') + 'input_imgs': Decimal('200.52'), }, # 1M tokens } def calculate_price( - self, version: str, input_tokens: int, output_tokens: int, image: FieldFile + self, version: str, input_tokens: int, output_tokens: int, image: FieldFile ) -> Decimal: price_map = self.TOKENS_COST[version.split('/')[1]] price = ( - input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 + input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 ) if image: price += price_map['input_imgs'] / 1_000 @@ -97,13 +94,15 @@ class Llama(SimpleService): msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[dict[str, str | list]]: + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[1:message_limit + 1] + ).order_by('-created_at')[1 : message_limit + 1] ) ) elif isinstance(self.store, APIStore): @@ -65,7 +65,9 @@ class Ltx(SimpleService): 'prompt': input_message.content, 'resolution': resolution, 'duration': duration, - 'camera_motion': camera_motion[input_message.info.pop('camera_motion', 'Без движения камеры')], + 'camera_motion': camera_motion[ + input_message.info.pop('camera_motion', 'Без движения камеры') + ], **input_message.info, } ) @@ -8,7 +8,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import RequestBlocked, GenerationException from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -81,7 +81,7 @@ class Midjourney(SimpleService): OUTPUT: Return ONLY a single concise image-generation prompt describing the final scene. """ - callback_data = dict(prompt=activation_prompt, prompt_optimizer=False,**input_message.info) + callback_data = dict(prompt=activation_prompt, prompt_optimizer=False, **input_message.info) results = replicate_run(self._CALLBACK, callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message=input_message) @@ -6,10 +6,8 @@ from typing import Any import requests from django.core.files import File -from django.utils.translation import gettext as _ from messages.models import Message -from ml_model.exceptions import RequestBlocked, InvalidParameterError, GenerationException from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -6,10 +6,8 @@ from typing import Any import requests from django.core.files import File -from django.utils.translation import gettext as _ from messages.models import Message -from ml_model.exceptions import RequestBlocked, InvalidParameterError, GenerationException from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -10,7 +10,6 @@ import requests from django.core.files import File 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 @@ -43,7 +42,9 @@ class Minimaxvideo(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: version = 'video-01' - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST[version]: + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST[ + version + ]: raise InsufficientBalance(balance, self.TOKENS_COST[version]) callback_data = dict({'prompt': input_message.content, **input_message.info}) if input_message.file: @@ -29,7 +29,7 @@ class MinIOService: endpoint=settings.MINIO_ENDPOINT, access_key=settings.MINIO_ACCESS_KEY, secret_key=settings.MINIO_SECRET_KEY, - secure=settings.MINIO_USE_HTTPS + secure=settings.MINIO_USE_HTTPS, ) def put_object(self, obj: BytesIO, filename: str, dest: str) -> str: @@ -10,12 +10,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import ( - ImageContentNotFound, - GenerationException, - ModelCouldNotInterpretPrompt, - RequestBlocked, -) from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -10,12 +10,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import ( - GenerationException, - ImageContentNotFound, - ModelCouldNotInterpretPrompt, - RequestBlocked, -) from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -4,7 +4,7 @@ from typing import Any import httpx from django.conf import settings -from ml_model.exceptions import OpenAIResponseError, ServiceTemporaryUnavailableError +from ml_model.exceptions import OpenAIResponseError, RequestBlocked, ServiceTemporaryUnavailableError from poller.models import Proxy type OpenAIEvent = dict[str, Any] @@ -27,7 +27,7 @@ class OpenAIStreamMixin: input_tokens, output_tokens = yield from self._stream_request( client, 'POST', json=payload, state=state ) - except ServiceTemporaryUnavailableError: + except (RequestBlocked, ServiceTemporaryUnavailableError): raise except Exception: if not state['response_id']: @@ -99,7 +99,10 @@ class OpenAIStreamMixin: case 'response.output_text.delta': return self._get_delta(event) case 'response.failed': - raise ServiceTemporaryUnavailableError from self._get_response_error(event) + error = self._get_response_error(event) + if error.code in ('content_filter', 'cyber_policy'): + raise RequestBlocked from error + raise ServiceTemporaryUnavailableError from error case 'response.completed' | 'response.incomplete': return self._get_usage(event) case _: @@ -23,23 +23,19 @@ class Perplexity(SimpleService): """ TOKENS_COST = { - 'sonar': { - 'input': Decimal('300'), - 'output': Decimal('300'), - 'search': Decimal('1500') - }, # 1M tokens + 'sonar': {'input': Decimal('300'), 'output': Decimal('300'), 'search': Decimal('1500')}, # 1M tokens 'sonar-deep-research': { 'input': Decimal('600'), # 1M tokens - 'output': Decimal('2400'), # 1M tokens - 'citation': Decimal('600'), # 1M tokens - 'search': Decimal('1500'), # 1K queries - 'reasoning': Decimal('900') # 1M tokens + 'output': Decimal('2400'), # 1M tokens + 'citation': Decimal('600'), # 1M tokens + 'search': Decimal('1500'), # 1K queries + 'reasoning': Decimal('900'), # 1M tokens }, 'sonar-pro-search': { 'input': Decimal('900'), # 1M tokens 'output': Decimal('4500'), # 1M tokens 'search': Decimal('5400'), # 1K queries - } + }, } def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: @@ -78,9 +74,11 @@ class Perplexity(SimpleService): try: for proxy in Proxy.objects.all(): response = httpx.post( - url='https://openrouter.ai/api/v1/chat/completions', headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, - proxy=f'{proxy.protocol}://{proxy.address}', timeout=600, - json={'model': version, 'messages': messages, **callback_data} + url='https://openrouter.ai/api/v1/chat/completions', + headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, + proxy=f'{proxy.protocol}://{proxy.address}', + timeout=600, + json={'model': version, 'messages': messages, **callback_data}, ) if response.status_code not in (200, 201): raise @@ -91,19 +89,27 @@ class Perplexity(SimpleService): content = ( re.sub( r'\[(\d+)\]', - lambda m: f' [[{m.group(1)}]]({str(annotations[int(m.group(1)) - 1]["url_citation"]["url"])})', - content + lambda m: ( + f' [[{m.group(1)}]]({str(annotations[int(m.group(1)) - 1]["url_citation"]["url"])})' + ), + content, ) - + f'\n\n### Ресурсы:\n{"\n".join( - [ - f'{num}. {a["url_citation"]["title"]} ({a["url_citation"]["url"]})' - for num, a in enumerate(annotations, start=1) - ] - )}' + + f'\n\n### Ресурсы:\n{ + "\n".join( + [ + f"{num}. {a['url_citation']['title']} ({a['url_citation']['url']})" + for num, a in enumerate(annotations, start=1) + ] + ) + }' ) - result = [content, response['usage']['prompt_tokens'], response['usage']['completion_tokens']] + result = [ + content, + response['usage']['prompt_tokens'], + response['usage']['completion_tokens'], + ] except Exception: - raise Exception(f'No answer from Perplexity, please retry later') + raise Exception('No answer from Perplexity, please retry later') process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, @@ -114,13 +120,15 @@ class Perplexity(SimpleService): msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[dict[str, str | list]]: + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[1:message_limit + 1] + ).order_by('-created_at')[1 : message_limit + 1] ) ) elif isinstance(self.store, APIStore): @@ -146,4 +154,4 @@ class Perplexity(SimpleService): while character_length > max_character_limit: character_length -= len(memory.pop(0)['content']) - return memory \ No newline at end of file + return memory @@ -13,9 +13,6 @@ from django.core.files import File from messages.models import Message from ml_model.exceptions import ( - ImageContentNotFound, - GenerationException, - RequestBlocked, FileTooLargeError, ) from ml_model.services.base import SimpleService @@ -9,7 +9,6 @@ from typing import Any from django.core.files import File 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 @@ -11,7 +11,7 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import GenerationException, RequestBlocked +from ml_model.exceptions import RequestBlocked from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -10,7 +10,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import GenerationException, RequestBlocked from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -1,7 +1,7 @@ import time from datetime import timedelta from decimal import Decimal -from typing import Any, Dict, Iterator +from typing import Any, Iterator from messages.models import Message from ml_model.exceptions import ModelVersionNotAvailable @@ -19,14 +19,8 @@ class Qwen(SimpleService): """ TOKENS_COST = { - 'qwq-32b': { - 'input': Decimal('45'), - 'output': Decimal('60') - }, # 1M tokens - 'qwq-32b:free': { - 'input': Decimal('0'), - 'output': Decimal('0') - }, # 1M tokens + 'qwq-32b': {'input': Decimal('45'), 'output': Decimal('60')}, # 1M tokens + 'qwq-32b:free': {'input': Decimal('0'), 'output': Decimal('0')}, # 1M tokens } def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: @@ -73,8 +67,8 @@ class Qwen(SimpleService): 'На бытовые, нейтральные или социальные вопросы (например: "что делаешь?", "как дела?") можно отвечать\n' 'Все размышления и логика перед ответом — на русском. Другой язык разрешён только в цитатах или если вопрос явно на другом языке.\n"' 'Не пересматривай прошлые примеры ответов, оценивай только текущий запрос. Не повторяй одни и те же выводы многократно.' - ) - } + ), + }, ) messages.append({'role': 'user', 'content': input_message.content}) result = openrouter_run(version, messages, callback_data, 'Qwen') @@ -88,13 +82,15 @@ class Qwen(SimpleService): msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[dict[str, str | list]]: + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[1:message_limit+1] + ).order_by('-created_at')[1 : message_limit + 1] ) ) elif isinstance(self.store, APIStore): @@ -1,7 +1,7 @@ import time from datetime import timedelta from decimal import Decimal -from typing import Any, Dict, Iterator +from typing import Any, Iterator from messages.models import Message from ml_model.exceptions import ModelVersionNotAvailable @@ -19,18 +19,9 @@ class Qwen_235B(SimpleService): """ TOKENS_COST = { - 'qwen3-235b-a22b-thinking-2507': { - 'input': Decimal('33'), - 'output': Decimal('180') - }, # 1M tokens - 'qwen3-235b-a22b-2507': { - 'input': Decimal('24'), - 'output': Decimal('165') - }, # 1M tokens - 'qwen3-235b-a22b:free': { - 'input': Decimal('0'), - 'output': Decimal('0') - }, # 1M tokes + 'qwen3-235b-a22b-thinking-2507': {'input': Decimal('33'), 'output': Decimal('180')}, # 1M tokens + 'qwen3-235b-a22b-2507': {'input': Decimal('24'), 'output': Decimal('165')}, # 1M tokens + 'qwen3-235b-a22b:free': {'input': Decimal('0'), 'output': Decimal('0')}, # 1M tokes } def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: @@ -77,8 +68,8 @@ class Qwen_235B(SimpleService): 'На бытовые, нейтральные или социальные вопросы (например: "что делаешь?", "как дела?") можно отвечать\n' 'Все размышления и логика перед ответом — на русском. Другой язык разрешён только в цитатах или если вопрос явно на другом языке.\n"' 'Не пересматривай прошлые примеры ответов, оценивай только текущий запрос. Не повторяй одни и те же выводы многократно.' - ) - } + ), + }, ) messages.append({'role': 'user', 'content': input_message.content}) result = openrouter_run(version, messages, callback_data, 'Qwen') @@ -92,13 +83,15 @@ class Qwen_235B(SimpleService): msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[dict[str, str | list]]: + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[1:message_limit+1] + ).order_by('-created_at')[1 : message_limit + 1] ) ) elif isinstance(self.store, APIStore): @@ -162,4 +162,4 @@ class Qwen_3_6(SimpleService): while character_length > max_character_limit: character_length -= len(memory.pop(0)['content']) - return memory \ No newline at end of file + return memory @@ -237,7 +237,6 @@ class Qwen_3_7(StreamSimpleService): def _estimate_cost(self, version_slug: str, input_tokens: int, output_tokens: int) -> float: price_map = self.TOKENS_COST[version_slug] price = ( - input_tokens * price_map['input'] / 1_000_000 - + output_tokens * price_map['output'] / 1_000_000 + input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 ) return float(price / self.COEFFICIENT) @@ -17,11 +17,13 @@ class Qwen_3_Max_Thinking(SimpleService): } def calculate_price(self, input_tokens: int, output_tokens: int) -> Decimal: - price = input_tokens * ( - self.TOKENS_COST['input']['default' if input_tokens <= 32_000 else 'high'] / 1_000_000 - ) + output_tokens * ( - self.TOKENS_COST['output']['default' if input_tokens <= 32_000 else 'high'] / 1_000_000 - ) + Decimal('2') + price = ( + input_tokens + * (self.TOKENS_COST['input']['default' if input_tokens <= 32_000 else 'high'] / 1_000_000) + + output_tokens + * (self.TOKENS_COST['output']['default' if input_tokens <= 32_000 else 'high'] / 1_000_000) + + Decimal('2') + ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, content: str, time: timedelta, save: bool = True) -> list[Message]: @@ -88,4 +90,4 @@ class Qwen_3_Max_Thinking(SimpleService): while character_length > max_character_limit: character_length -= len(memory.pop(0)['content']) - return memory \ No newline at end of file + return memory @@ -30,10 +30,8 @@ from ml_model.exceptions import ( ExceededContextLengthError, CorruptedFileError, ) -from ml_model.models import NeuronModel from ml_model.services.chatgpt_4 import Chatgpt_4 -from django.core.files.uploadedfile import UploadedFile from datetime import timedelta @@ -212,10 +210,10 @@ class Raifgpt(Chatgpt_4): input = [ SystemMessage(content=user_system_prompt), HumanMessage( - content=( - 'Используй системный промпт. Содержание файла: ' - f'{"".join(chunk.content for chunk in chunks)}. Вопрос: {input_message.content}' - ) + content=( + 'Используй системный промпт. Содержание файла: ' + f'{"".join(chunk.content for chunk in chunks)}. Вопрос: {input_message.content}' + ) ), ] input_tokens += self.count_text_tokens(input) @@ -263,11 +261,11 @@ class Raifgpt(Chatgpt_4): image_count = 0 doc = None try: - doc = fitz.open(stream=pdf_data, filetype="pdf") + doc = fitz.open(stream=pdf_data, filetype='pdf') raw_texts = {} pages_with_image = [] for page_num, page in enumerate(doc): - text = page.get_text("text") + text = page.get_text('text') if text: raw_texts[page_num] = text if page.get_images(): @@ -276,28 +274,28 @@ class Raifgpt(Chatgpt_4): if doc is not None: doc.close() fitz.TOOLS.store_shrink(100) - all_text = "\n".join(raw_texts.get(i, "") for i in sorted(raw_texts)) - return all_text if all_text.strip() else "Не удалось извлечь текст из PDF" + all_text = '\n'.join(raw_texts.get(i, '') for i in sorted(raw_texts)) + return all_text if all_text.strip() else 'Не удалось извлечь текст из PDF' except Exception as e: if doc is not None: doc.close() - return f"Ошибка при чтении PDF: {e}" + return f'Ошибка при чтении PDF: {e}' try: batch_images = [] page_index_map = [] headers = { - "Authorization": f"Api-Key {settings.YANDEX_CLOUD_API_KEY}", - "Content-Type": "application/json" + 'Authorization': f'Api-Key {settings.YANDEX_CLOUD_API_KEY}', + 'Content-Type': 'application/json', } for page_num in pages_with_image: try: page = doc.load_page(page_num) image_count += 1 pix = page.get_pixmap(dpi=150, alpha=False) - img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) + img = Image.frombytes('RGB', [pix.width, pix.height], pix.samples) img.info = {} buffer = BytesIO() - img.save(buffer, format="JPEG", quality=60, optimize=True) + img.save(buffer, format='JPEG', quality=60, optimize=True) buffer.seek(0) if buffer.getbuffer().nbytes < max_batch_size: batch_images.append(buffer) @@ -308,7 +306,7 @@ class Raifgpt(Chatgpt_4): if doc is not None: doc.close() fitz.TOOLS.store_shrink(100) - return "Не удалось собрать изображения из PDF." + return 'Не удалось собрать изображения из PDF.' batches = [] current_batch = [] current_pages = [] @@ -326,56 +324,62 @@ class Raifgpt(Chatgpt_4): ocr_texts = {} for batch, pages in batches: body = { - "folderId": settings.YANDEX_CLOUD_ID, - "analyze_specs": [{ - "content": base64.b64encode(buf.getvalue()).decode(), - "features": [{ - "type": "TEXT_DETECTION", - "text_detection_config": {"language_codes": ["*"]} - }] - } for buf in batch] + 'folderId': settings.YANDEX_CLOUD_ID, + 'analyze_specs': [ + { + 'content': base64.b64encode(buf.getvalue()).decode(), + 'features': [ + { + 'type': 'TEXT_DETECTION', + 'text_detection_config': {'language_codes': ['*']}, + } + ], + } + for buf in batch + ], } resp = requests.post( - "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze", - headers=headers, json=body, timeout=60 + 'https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze', + headers=headers, + json=body, + timeout=60, ) if resp.status_code != 200: continue result = resp.json() - for i, spec_result in enumerate(result.get("results", [])): + for i, spec_result in enumerate(result.get('results', [])): page_text = [] - for res in spec_result.get("results", []): - for page in res.get("textDetection", {}).get("pages", []): + for res in spec_result.get('results', []): + for page in res.get('textDetection', {}).get('pages', []): for block in page.get('blocks', []): for line in block.get('lines', []): - line_text = " ".join( + line_text = ' '.join( word.get('text', '') for word in line.get('words', []) ) if line_text: page_text.append(line_text) - ocr_texts[pages[i]] = "\n".join(page_text) + ocr_texts[pages[i]] = '\n'.join(page_text) if doc is not None: doc.close() fitz.TOOLS.store_shrink(100) all_pages = sorted(set(raw_texts) | set(ocr_texts)) - final_text = "\n\n".join( - f"{raw_texts.get(pn, '')}\n{ocr_texts.get(pn, '')}".strip() - for pn in all_pages + final_text = '\n\n'.join( + f'{raw_texts.get(pn, "")}\n{ocr_texts.get(pn, "")}'.strip() for pn in all_pages ) self.image_count = image_count - return final_text.strip() or "Не удалось распознать текст" + return final_text.strip() or 'Не удалось распознать текст' except Exception as e: if doc is not None: doc.close() - return f"Не удалось обработать файл: {e}" + return f'Не удалось обработать файл: {e}' def make_embeddings_prompt(self, document_name: str, section_texts: List[str], question: str) -> str: - ''' + """ A method for making a prompt using found embeddings :param document_name: name of the loaded document :param section_texts: list of sections' contents :param question: user question - ''' + """ return f"""Ты — аналитик данных моей компании. Отвечай исключительно на основе предоставленного ниже контекста. НЕЛЬЗЯ использовать внешние знания или домыслы. @@ -394,7 +398,9 @@ class Raifgpt(Chatgpt_4): Сформируй ПОЛНЫЙ и СТРУКТУРИРОВАННЫЙ ответ, даже если доступные данные частичные. """ - def get_anchor_embedding(self, client: httpx.Client, content: str, anchor: str) -> Tuple[List[float], int, str]: + def get_anchor_embedding( + self, client: httpx.Client, content: str, anchor: str + ) -> Tuple[List[float], int, str]: """ A method for converting raw text (anchor content) into embeddings using OpenAI API request @@ -434,4 +440,3 @@ class Raifgpt(Chatgpt_4): return f'Это текст, извлечённый из загруженного WORD-файла:\n{text}' else: return 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' - @@ -10,7 +10,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import RequestBlocked, GenerationException from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -50,11 +50,11 @@ class Reve(SimpleService): # return self.PRICE[type].quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, - prompt: str, - image: str, - time: timedelta, - save: bool = True, + self, + prompt: str, + image: str, + time: timedelta, + save: bool = True, ) -> list[Message]: messages: list[Message] = [] messages.append( @@ -10,7 +10,7 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import FileNotProvided, RequestBlocked, GenerationException +from ml_model.exceptions import FileNotProvided from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -19,7 +19,6 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Runway(SimpleService): - TOKENS_COST = { 'gen4-turbo': Decimal('15'), } @@ -50,9 +49,8 @@ class Runway(SimpleService): version = 'gen4-turbo' duration = input_message.info.get('duration', 5) file = input_message.file - if ( - (balance := PaymentPlanSelector(self.store.user).get_current_balance()) - < (cost := self.calculate_price(version, duration)) + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.calculate_price(version, duration) ): raise InsufficientBalance(balance, cost) if not file: @@ -65,7 +63,7 @@ class Runway(SimpleService): callback_data = { 'prompt': self.translate_prompt(input_message.content), **input_message.info, - 'image': media + 'image': media, } start_time = time.time() video = replicate_run(f'runwayml/{version}', callback_data) @@ -205,4 +205,4 @@ class Seedance_2_Dreamina(SimpleService): text=True, check=True, ) - return float(json.loads(out.stdout)["format"]["duration"]) \ No newline at end of file + return float(json.loads(out.stdout)['format']['duration']) @@ -13,7 +13,6 @@ import requests from django.core.files import File 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 @@ -18,9 +18,7 @@ from ml_model.services.base import SimpleService from poller.models import Proxy - class Sora(SimpleService): - TOKENS_COST = { 'sora-2': Decimal('30'), 'sora-2-pro': Decimal('90'), @@ -48,7 +46,7 @@ class Sora(SimpleService): return Message.objects.bulk_create([msg]) return [msg] - def make(self, input_message: "Message", save: bool = True) -> list["Message"]: + def make(self, input_message: 'Message', save: bool = True) -> list['Message']: if input_message.content: for proxy in Proxy.objects.all(): version = input_message.info.pop('version', None) @@ -59,7 +57,7 @@ class Sora(SimpleService): 'prompt': input_message.content, 'model': version, 'seconds': str(seconds), - **input_message.info + **input_message.info, } files = None if input_message.file: @@ -73,47 +71,39 @@ class Sora(SimpleService): if current_size != required_size: raise UnsupportedSize(current_size, required_size) buf = BytesIO() - img.save(buf, format="PNG") + img.save(buf, format='PNG') buf.seek(0) - files = { - "input_reference": ( - input_message.file.name, - buf, - mime_type - ) - } + files = {'input_reference': (input_message.file.name, buf, mime_type)} with httpx.Client( - base_url='https://api.openai.com/v1/', - proxy=f'{proxy.protocol}://{proxy.address}' if proxy else None, - headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, - timeout=600, + base_url='https://api.openai.com/v1/', + proxy=f'{proxy.protocol}://{proxy.address}' if proxy else None, + headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, + timeout=600, ) as client: start_time = time.time() if files: - resp = client.post("videos", data=callback_data, files=files) + resp = client.post('videos', data=callback_data, files=files) else: - resp = client.post("videos", json=callback_data) + resp = client.post('videos', json=callback_data) if resp.status_code not in (200, 201): continue video_info = resp.json() - video_id = video_info.get("id") + video_id = video_info.get('id') while True: - status_resp = client.get(f"videos/{video_id}") - status = status_resp.json().get("status") + status_resp = client.get(f'videos/{video_id}') + status = status_resp.json().get('status') video_data = status_resp.json() - if status == "completed": + if status == 'completed': break - if status == "failed": - error_message = video_data.get("error", {}).get("message", "Unknown error") - if error_message == "Your request was blocked by our moderation system.": + if status == 'failed': + error_message = video_data.get('error', {}).get('message', 'Unknown error') + if error_message == 'Your request was blocked by our moderation system.': raise RequestBlocked - raise Exception("Video generation failed") - time.sleep(1/3) - video = client.get(f"videos/{video_id}/content").content + raise Exception('Video generation failed') + time.sleep(1 / 3) + video = client.get(f'videos/{video_id}/content').content process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, seconds, version - ) + self.handle_invoice(input_message.content_object.model, seconds, version) msgs = self.save_results(input_message.content, process_time, video, save) return msgs raise ModelTimeoutError @@ -35,4 +35,4 @@ class Stablediffusion(Seedream): def make(self, input_message: Message, save: bool = True) -> list[Message]: input_message.info = self._remap_info(input_message.info) - return super().make(input_message, save) \ No newline at end of file + return super().make(input_message, save) @@ -10,7 +10,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import RequestBlocked, GenerationException from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -18,32 +18,29 @@ from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector - class Video_Test_Model(SimpleService): - RATES = { 'per-unit': Decimal('15'), 'per-second': Decimal('0.5'), } - - - PLACEHOLDER_URL='https://imgur.com/QPLhtj1.mp4' - def calculate_price(self, strategy: Literal['per-unit','per-second'], duration: int = 1, num_videos: int = 1) -> Decimal: + PLACEHOLDER_URL = 'https://imgur.com/QPLhtj1.mp4' + + def calculate_price( + self, strategy: Literal['per-unit', 'per-second'], duration: int = 1, num_videos: int = 1 + ) -> Decimal: rate = self.RATES[strategy] return (rate * duration * num_videos).quantize(Decimal('0.1'), rounding='ROUND_UP') - @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: num_videos = info.get('num_videos', 1) cps = info.get('cps', 'per-unit') - + if cps == 'per-second': return None - - return cls.RATES['per-unit'] * num_videos + return cls.RATES['per-unit'] * num_videos def save_results( self, @@ -66,75 +63,71 @@ class Video_Test_Model(SimpleService): return Message.objects.bulk_create(messages) return messages - def make(self, input_message: Message, save: bool = True) -> list[Message]: cps = input_message.info.get('cps', 'per-unit') cvu = input_message.info.get('cvu') or self.PLACEHOLDER_URL num_videos = input_message.info.get('num_videos', 1) - + start_time = time.time() video_bytes = self._fetch_video(cvu) - + if cps == 'per-second': duration = self._get_duration(video_bytes) else: duration = 1 - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < (cost := self.calculate_price(cps, duration, num_videos)): + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.calculate_price(cps, duration, num_videos) + ): raise InsufficientBalance(balance, cost) - - + videos = [video_bytes] * num_videos - + process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, cps, duration, num_videos) - + msgs = self.save_results(input_message.content, process_time, videos, save) return msgs - def _fetch_video(self, url: str): headers = { - "User-Agent": ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/137.0 Safari/537.36" + 'User-Agent': ( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/137.0 Safari/537.36' ) } try: - response = requests.get( - url, - headers=headers, - timeout=600 - ) + response = requests.get(url, headers=headers, timeout=600) response.raise_for_status() except requests.RequestException as exc: raise InvalidParameterError(f'Invalid video URL: {exc}') - + kind = filetype.guess(response.content[:120]) - + if not kind: raise CorruptedFileError - + if not kind.mime.startswith('video/'): raise InvalidParameterError('Video format not supported') - + return response.content - def _get_duration(self, video_bytes: bytes) -> int: result = subprocess.run( [ - "ffprobe", - "-v", "quiet", - "-print_format", "json", - "-show_format", - "-", + 'ffprobe', + '-v', + 'quiet', + '-print_format', + 'json', + '-show_format', + '-', ], input=video_bytes, capture_output=True, ) data = json.loads(result.stdout) - return math.ceil(float(data["format"]["duration"])) \ No newline at end of file + return math.ceil(float(data['format']['duration'])) @@ -10,7 +10,7 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError, GenerationException +from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -42,7 +42,9 @@ class Wan_Lite(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: resolution = input_message.info.pop('resolution', '720p') - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST[resolution]: + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST[ + resolution + ]: raise InsufficientBalance(balance, self.TOKENS_COST[resolution]) callback_data = dict( { @@ -59,103 +59,103 @@ In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend """ ANCHORS = { - "authors": ( - "автор|авторы|составители|подготовители|команда|коллектив|исследователь|" - "исследователи|авторский коллектив|writer|researcher|investigator|" - "contributors|исполнители|ответственные лица|authorship|авторство|" - "group|team|authorship team", - 2 - ), - "topic": ( - "тема исследования|предмет исследования|тема работы|цель исследования|" - "предмет|направление|scope|research topic|subject of study|research focus|" - "object of study|scientific problem|область исследования|problem statement", - 2 - ), - "summary": ( - "краткое содержание|основные выводы|итоги исследования|summary|conclusions|" - "executive summary|highlights|abstract|overview|synopsis|выводы|" - "резюме|summary statement", - 6 - ), - "volume": ( - "объем рынка|market size|размер рынка|объем продаж|общие показатели|" - "объем инвестиций|total volume|market volume|рыночная капитализация|" - "оборот|объем финансирования|масштаб рынка|market capacity", - 4 - ), - "forecast": ( - "прогноз|forecast|прогнозные показатели|ожидания|перспективы|outlook|" - "прогноз развития|predicted values|прогноз роста|прогноз падения|" - "future outlook|прогноз на следующий год|прогноз на 3-5 лет", - 4 - ), - "growth_drivers": ( - "драйверы роста|факторы роста|причины роста|growth drivers|" - "growth factors|catalysts|key drivers|стимулирующие факторы|" - "факторы развития|движущие силы|причины повышения|рост рынка|growth enablers", - 4 - ), - "barriers": ( - "барьеры|препятствия|ограничения|риски|сложности|ограничения рынка|" - "barriers|obstacles|challenges|risks|факторы замедления|факторы риска|" - "рисковые факторы|проблемы|тормозящие развитие|негативные факторы", - 4 - ), - "regulations": ( - "регуляторные изменения|законодательство|нормативные акты|регулирование|" - "compliance|laws|regulations|legal changes|закон|правила|постановления|" - "стандарты|регулирующие органы|политические инициативы|правовые нормы", - 4 - ), - "segmentation": ( - "сегментация|разделение рынка|сегменты|группы клиентов|customer segments|" - "market segmentation|категории|подразделения|типы клиентов|демографические" - " группы|целевые аудитории|сегментация по регионам|product segmentation", - 4 - ), - "players": ( - "игроки рынка|компании|корпорации|основные участники|конкуренты|market " - "players|key companies|competitors|поставщики|лидеры рынка|крупные компании|" - "бизнес-игроки|участники рынка|основные бренды", - 4 - ), - "quant_metrics": ( - "количественные метрики|числовые показатели|quantitative metrics|цифры|" - "data points|измерения|показатели|объемы|количество сделок|темпы роста|" - "проценты|значения|финансовые показатели|статистика", - 2 - ), - "qual_metrics": ( - "качественные метрики|качественные показатели|qualitative metrics|оценки" - "|факторы оценки|quality indicators|мнение экспертов|экспертные оценки|" - "восприятие|качественные данные|отзывы|качественный анализ", - 2 - ), - "cases": ( - "кейсы|примеры|практические примеры|case studies|examples|use cases|" - "проекты|сценарии|успешные истории|best practices|опыт применения", - 1 - ), - "charts": ( - "графики|диаграммы|charts|diagrams|visualizations|plots|иллюстрации|" - "схемы|инфографика|data visualization|charts and graphs", - 1 - ), - "tables": ( - "таблицы|data tables|таблицы данных|spreadsheets|matrices|таблицы с данными|" - "табличные данные|списки|структуры данных|табличное представление", - 1 - ), - "methodology": ( - "методология|методы исследования|approach|methodology|methods|techniques|" - "исследовательские методы|методики|способы анализа|процедура|процесс исследования", - 1 - ), - "interviews": ( - "интервью|мнения экспертов|комментарии|expert interviews|expert opinions|" - "statements|reviews|опросы|интервью с экспертами|экспертные отзывы|" - "интервьюирование|отзывы участников", - 1 + 'authors': ( + 'автор|авторы|составители|подготовители|команда|коллектив|исследователь|' + 'исследователи|авторский коллектив|writer|researcher|investigator|' + 'contributors|исполнители|ответственные лица|authorship|авторство|' + 'group|team|authorship team', + 2, + ), + 'topic': ( + 'тема исследования|предмет исследования|тема работы|цель исследования|' + 'предмет|направление|scope|research topic|subject of study|research focus|' + 'object of study|scientific problem|область исследования|problem statement', + 2, + ), + 'summary': ( + 'краткое содержание|основные выводы|итоги исследования|summary|conclusions|' + 'executive summary|highlights|abstract|overview|synopsis|выводы|' + 'резюме|summary statement', + 6, + ), + 'volume': ( + 'объем рынка|market size|размер рынка|объем продаж|общие показатели|' + 'объем инвестиций|total volume|market volume|рыночная капитализация|' + 'оборот|объем финансирования|масштаб рынка|market capacity', + 4, + ), + 'forecast': ( + 'прогноз|forecast|прогнозные показатели|ожидания|перспективы|outlook|' + 'прогноз развития|predicted values|прогноз роста|прогноз падения|' + 'future outlook|прогноз на следующий год|прогноз на 3-5 лет', + 4, + ), + 'growth_drivers': ( + 'драйверы роста|факторы роста|причины роста|growth drivers|' + 'growth factors|catalysts|key drivers|стимулирующие факторы|' + 'факторы развития|движущие силы|причины повышения|рост рынка|growth enablers', + 4, + ), + 'barriers': ( + 'барьеры|препятствия|ограничения|риски|сложности|ограничения рынка|' + 'barriers|obstacles|challenges|risks|факторы замедления|факторы риска|' + 'рисковые факторы|проблемы|тормозящие развитие|негативные факторы', + 4, + ), + 'regulations': ( + 'регуляторные изменения|законодательство|нормативные акты|регулирование|' + 'compliance|laws|regulations|legal changes|закон|правила|постановления|' + 'стандарты|регулирующие органы|политические инициативы|правовые нормы', + 4, + ), + 'segmentation': ( + 'сегментация|разделение рынка|сегменты|группы клиентов|customer segments|' + 'market segmentation|категории|подразделения|типы клиентов|демографические' + ' группы|целевые аудитории|сегментация по регионам|product segmentation', + 4, + ), + 'players': ( + 'игроки рынка|компании|корпорации|основные участники|конкуренты|market ' + 'players|key companies|competitors|поставщики|лидеры рынка|крупные компании|' + 'бизнес-игроки|участники рынка|основные бренды', + 4, + ), + 'quant_metrics': ( + 'количественные метрики|числовые показатели|quantitative metrics|цифры|' + 'data points|измерения|показатели|объемы|количество сделок|темпы роста|' + 'проценты|значения|финансовые показатели|статистика', + 2, + ), + 'qual_metrics': ( + 'качественные метрики|качественные показатели|qualitative metrics|оценки' + '|факторы оценки|quality indicators|мнение экспертов|экспертные оценки|' + 'восприятие|качественные данные|отзывы|качественный анализ', + 2, + ), + 'cases': ( + 'кейсы|примеры|практические примеры|case studies|examples|use cases|' + 'проекты|сценарии|успешные истории|best practices|опыт применения', + 1, + ), + 'charts': ( + 'графики|диаграммы|charts|diagrams|visualizations|plots|иллюстрации|' + 'схемы|инфографика|data visualization|charts and graphs', + 1, + ), + 'tables': ( + 'таблицы|data tables|таблицы данных|spreadsheets|matrices|таблицы с данными|' + 'табличные данные|списки|структуры данных|табличное представление', + 1, + ), + 'methodology': ( + 'методология|методы исследования|approach|methodology|methods|techniques|' + 'исследовательские методы|методики|способы анализа|процедура|процесс исследования', + 1, + ), + 'interviews': ( + 'интервью|мнения экспертов|комментарии|expert interviews|expert opinions|' + 'statements|reviews|опросы|интервью с экспертами|экспертные отзывы|' + 'интервьюирование|отзывы участников', + 1, ), } @@ -144,7 +144,9 @@ class NeuronModel(BaseModel, OrderedModel): @property def service(self) -> 'SimpleService': # noqa: F821 - return getattr(importlib.import_module(f'ml_model.services.{self.slug}'), self.slug.replace('-', '').title()) + return getattr( + importlib.import_module(f'ml_model.services.{self.slug}'), self.slug.replace('-', '').title() + ) @property def streaming(self) -> bool: @@ -62,9 +62,9 @@ class NeuronModelAPIView(APIView): selector = NeuronModelSelector(request.user) model = selector.get_model_by_slug(slug=slug, hidden=False) if not selector.get_model_accessible_status(model): - return Response({'detail': _('Model data cannot be retrieved')}, status=status.HTTP_403_FORBIDDEN) - return Response( - NeuronModelSerializer(model).data - ) + return Response( + {'detail': _('Model data cannot be retrieved')}, status=status.HTTP_403_FORBIDDEN + ) + return Response(NeuronModelSerializer(model).data) except (NeuronModelNotExist, Exception) as exc: return Response({'detail': f'{exc}'}, status=status.HTTP_400_BAD_REQUEST) @@ -1,5 +1,6 @@ from django.utils.translation import gettext as _ + class FullBalanceException(Exception): def __str__(self) -> str: - return _('Your balance is already full') \ No newline at end of file + return _('Your balance is already full') @@ -12,12 +12,13 @@ class PaymentAttempt(BaseModel): verbose_name=_('Payment Method'), related_name='payment_attempts', ) + # FIXME: переименовать в reason + # FIXME: blank=True, null=True пересмотреть, т.к Attempt - это FailedAttempt и причина скорее всего есть всегдад cancel_reason = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Cancel Reason')) + # FIXME: связывать попытки на основании метадаты при повторных попытках оплаты + # FIXME: вместо флага должен быть trace-id или подобный атрибут, который будет явно описывать, какие попытки связаны in_cycle = models.BooleanField(default=True, verbose_name=_('In Cycle')) - def __str__(self) -> str: - return f'Attempt of ({self.method})\nReason: {self.cancel_reason}' - class Meta: verbose_name = _('Payment Attempt') verbose_name_plural = _('Payment Attempts') @@ -15,7 +15,9 @@ class PaymentPlanFeature(OrderedModel): plan = models.ForeignKey( PaymentPlan, on_delete=models.CASCADE, related_name='features', verbose_name=_('Payment Plan') ) - model = models.ForeignKey(NeuronModel, on_delete=models.CASCADE, related_name='feature', verbose_name=_('Neuron Model')) + model = models.ForeignKey( + NeuronModel, on_delete=models.CASCADE, related_name='feature', verbose_name=_('Neuron Model') + ) quantity = models.PositiveIntegerField(default=1, verbose_name=_('Quantity')) measurement_unit = models.CharField( max_length=15, @@ -26,12 +26,14 @@ class PaymentMethod(BaseModel): active = models.BooleanField(default=False, verbose_name=_('Active')) primary = models.BooleanField(default=True, verbose_name=_('Primary')) + # FIXME: в property не должно быть запросов, для использования property объект(-ы) должен быть аннотирован через .annotate @property def attempts(self): if hasattr(self, 'total_attempts'): return self.total_attempts return self.payment_attempts.filter(in_cycle=True).count() + # FIXME: должно быть перенесено на уровень сервисов, мы не изменяем метод save модели def save(self, *args, **kwargs): with transaction.atomic(): if self.primary: @@ -114,6 +114,7 @@ class PlansAPITest(BaseAuthorizedAPITest): def test_unauthorized_by_permission(self) -> None: from authentication.models import CustomUserModel + host_user = CustomUserModel.objects.create_user(email='test_2@test.test', password='test_2') host = BusinessUserHost.objects.create(user=host_user) BusinessAccount.objects.create(user=self.user, parent_company=host) @@ -239,4 +240,4 @@ class PlansAPITest(BaseAuthorizedAPITest): def test_tokens_per_plan(self) -> None: plans = self.get().json() for plan in plans: - self.assertGreater(Decimal(str(plan['tokens_per_plan'])), 0) \ No newline at end of file + self.assertGreater(Decimal(str(plan['tokens_per_plan'])), 0) @@ -79,4 +79,4 @@ class BalanceAPITest(BaseAuthorizedAPITest): ) business_account.save() balance = self.get().json()['current_token_balance'] - self.assertEqual(Decimal(balance), business_account.group.token_limit) \ No newline at end of file + self.assertEqual(Decimal(balance), business_account.group.token_limit) @@ -314,4 +314,3 @@ class ReferralAccountAdmin(admin.ModelAdmin): @admin.display(description='Получено бонусов') def _accrued_bonuses(self, obj: ReferralAccount): return f'{obj.accrued_bonuses.aggregate(total=Coalesce(Sum("amount"), Decimal(0), output_field=models.DecimalField()))["total"]} токенов' - @@ -30,4 +30,4 @@ def clear_recurrent_on_individual_plan_assignment( if not instance.plan.individual: return PaymentPlanUserInfo.objects.filter(pk=instance.pk).update(next_payment_at=None) - PaymentMethodService(instance.user).deactivate_payment_methods(notify=None) \ No newline at end of file + PaymentMethodService(instance.user).deactivate_payment_methods(notify=None) @@ -22,7 +22,10 @@ class SendErrorReportEmailAPIView(APIView): data = serializer.validated_data report = Report( message=data['report_text'], - attachments=[File(file.name, file.file, file.content_type, file.size) for file in data.get('images', [])] + attachments=[ + File(file.name, file.file, file.content_type, file.size) + for file in data.get('images', []) + ], ) EmailService(request.user).send_error_email(report) return Response({'detail': 'error report sent'}, status=status.HTTP_201_CREATED) @@ -76,4 +76,3 @@ class PublicSSEStoreService(SSEStoreService): def _get_cache_key(self) -> str: return f'sse:tokens:{self.user_uuid}:{self.message_uuid}:public' - \ No newline at end of file @@ -72,4 +72,3 @@ class MessageSchema(ModelSchema): if isinstance(value, str): return value return value.url - @@ -1,4 +1,3 @@ -import re import time from pathlib import Path @@ -19,4 +19,4 @@ class VoiceSchema(ModelSchema): class PresetSchema(ModelSchema): class Meta: model = Preset - fields = ('uid', 'title', 'file', 'metadata') \ No newline at end of file + fields = ('uid', 'title', 'file', 'metadata') @@ -3,4 +3,4 @@ from enum import Enum class PresetKindEnum(str, Enum): voice = 'voice' - instrumental = 'instrumental' \ No newline at end of file + instrumental = 'instrumental' @@ -119,7 +119,9 @@ def openai_responses_stream(request, body: dict): if not (model_ref := body.get('model')): raise HttpError(400, _('You must provide a model parameter')) model = _resolve_model(model_ref) - return _to_openai(public_stream_message(request, model.slug, _parse_body(body)), model_ref, request=request) + return _to_openai( + public_stream_message(request, model.slug, _parse_body(body)), model_ref, request=request + ) @OpenAIErrorService.view @@ -15,7 +15,7 @@ from tools.public_api.serializers import APIKeyResultSerializer class APIKeyService(BaseService): def create(self, payload: dict, serialize: bool = False) -> APIKey | APIKeyResultSerializer: - if not (self.user.account_type in ('business_host', 'regular', 'business_admin')): + if self.user.account_type not in ('business_host', 'regular', 'business_admin'): raise Exception('Can not create API key from business sub-account.') try: api_key = APIKey.objects.create(user=self.user, **payload) @@ -6,14 +6,25 @@ from ninja.errors import HttpError class OpenAIErrorService: TYPES = { - 400: 'invalid_request_error', 401: 'authentication_error', 403: 'permission_error', - 404: 'invalid_request_error', 409: 'invalid_request_error', 501: 'api_error', + 400: 'invalid_request_error', + 401: 'authentication_error', + 403: 'permission_error', + 404: 'invalid_request_error', + 409: 'invalid_request_error', + 501: 'api_error', } @classmethod def response(cls, status: int, message: str) -> JsonResponse: return JsonResponse( - {'error': {'message': str(message), 'type': cls.TYPES.get(status, 'api_error'), 'param': None, 'code': None}}, + { + 'error': { + 'message': str(message), + 'type': cls.TYPES.get(status, 'api_error'), + 'param': None, + 'code': None, + } + }, status=status, ) @@ -22,8 +22,7 @@ class BaseElevenlabsAPIView: def _authorization_headers(self, request): return { - 'Authorization': request.headers.get('Xi-Api-Key') - or request.headers.get('Authorization', ''), + 'Authorization': request.headers.get('Xi-Api-Key') or request.headers.get('Authorization', ''), } def _proxy_request(self, request, data=None): @@ -173,7 +173,9 @@ class OpenAIVoiceAPIView(PublicVoiceUploadAPIView, PublicVoiceListAPIView): name = request.data.get('name') sample = request.FILES.get('audio_sample') if not sample: - return _openai_error(_('Missing audio_sample.'), param='audio_sample', code='missing_required_parameter') + return _openai_error( + _('Missing audio_sample.'), param='audio_sample', code='missing_required_parameter' + ) proxy = type( 'RequestProxy', @@ -278,7 +280,9 @@ class OpenAIAudioSpeechAPIView(VoiceView): if st == status.HTTP_403_FORBIDDEN: return _openai_error(str(detail), err_type='permission_error', http_status=st) if st >= 500: - return _openai_error(str(detail), err_type='api_error', code='internal_error', http_status=st) + return _openai_error( + str(detail), err_type='api_error', code='internal_error', http_status=st + ) return _openai_error(str(detail), http_status=st) r = httpx.get(result.data[0]['file'], timeout=120.0) if r.status_code >= 400: @@ -129,5 +129,6 @@ django-minio-backend = { git = "https://github.com/theriverman/django-minio-back [tool.ruff.lint.per-file-ignores] "__init__.py" = ["E402", "F401"] +"apps.py" = ["F401"] "**/{tests,docs,tools}/*" = ["E402"] "backend/settings.py" = ["F403", "E402"]