@@ -2,13 +2,13 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-06 17:58+0300\n" +"POT-Creation-Date: 2026-05-10 16:35+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1049,7 +1049,7 @@ msgstr "Соответствующая версия не найдена" msgid "Image is ready" msgstr "Изображение готово" -#: ml_model/services/elevenlabs_music.py:46 +#: ml_model/services/elevenlabs_music.py:45 msgid "Duration cannot be less than 5 seconds" msgstr "Длительность не может быть меньше 5 секунд" @@ -1067,20 +1067,27 @@ msgstr "" "Режим «Плавное движение» доступен только для 5-секундных видео в качестве " "540p и 720p" -#: ml_model/services/minimaxmusic.py:58 -#: ml_model/services/minimaxmusic_lite.py:62 -msgid "Lyrics is too long" -msgstr "Текст песни слишком длинный" - #: ml_model/services/minio_service.py:37 ml_model/services/minio_service.py:55 #: ml_model/services/minio_service.py:63 ml_model/services/minio_service.py:72 msgid "Unknown bucket destination" msgstr "Неизвестный бакет для загрузки" +#: ml_model/services/seedance_2_dreamina.py:108 +msgid "1080p output is not supported for Seedance Dreamina 2.0 Fast." +msgstr "1080р разрешение не поддерживается для Seedance Dreamina 2.0 Fast." + +#: ml_model/services/seedream.py:78 +msgid "3K output is not supported for Seedream 4.5" +msgstr "3К разрешение не поддерживается для Seedream 4.5" + #: ml_model/services/upscaleai.py:124 msgid "No image given for improving" msgstr "Нет изображения для улучшения" +#: ml_model/tasks.py:137 +msgid "Lyrics is too long" +msgstr "Текст песни слишком длинный" + #: ml_model/views.py:65 msgid "Model data cannot be retrieved" msgstr "Невозможно получить данные модели" @@ -111,11 +111,22 @@ class BytedanceModelArkAdapter: def _raise_by_error_payload(cls, data: dict[str, Any], choices: list[dict[str, Any]]) -> None: choice_reasons = {str(choice.get('finish_reason', '')).lower() for choice in choices} + if cls._is_request_blocked_error(data): + raise RequestBlocked + if BytedanceFinishReason.CONTENT_FILTER in choice_reasons: raise RequestBlocked raise GenerationException + @staticmethod + def _is_request_blocked_error(data: dict[str, Any]) -> bool: + error = data.get('error') + if not isinstance(error, dict): + return False + + return str(error.get('code', '')) == 'InputTextSensitiveContentDetected' + @classmethod def _extract_chat_answer( @@ -179,7 +190,7 @@ class BytedanceModelArkAdapter: ) -> RunChatResult: payload = dict(callback_data) payload['model'] = model - payload['messages'] = messages or [] + payload['messages'] = messages for proxy in Proxy.objects.all(): with httpx.Client( base_url=cls.BASE_URL, @@ -198,7 +209,15 @@ class BytedanceModelArkAdapter: cls.CONTENT_TYPE_TO_ENDPOINT[BytedanceContentType.CHAT], resp.text, ) - data: BytedanceChatResponse = resp.json() + try: + data: BytedanceChatResponse = resp.json() + except Exception as exc: + logger.error( + 'Bytedance chat/completions status=%s response=%s', + resp.status_code, + resp.text, + ) + raise GenerationException from exc try: answer = cls._extract_chat_answer( data=data, @@ -210,9 +229,18 @@ class BytedanceModelArkAdapter: return answer, input_tokens, output_tokens except (RequestBlocked, GenerationException): + logger.error( + 'Bytedance chat/completions status=%s response=%s', + resp.status_code, + resp.text, + ) raise except Exception as exc: - logger.error('Invalid Bytedance Ark response: %s', data) + logger.error( + 'Bytedance chat/completions status=%s response=%s', + resp.status_code, + resp.text, + ) raise GenerationException from exc raise GenerationException @@ -246,11 +274,26 @@ class BytedanceModelArkAdapter: timeout=600, ) as client: resp = client.post(cls.CONTENT_TYPE_TO_ENDPOINT[BytedanceContentType.IMAGE], json=payload) - data = resp.json() + try: + data = resp.json() + except Exception as exc: + logger.error( + 'Bytedance images/generations status=%s response=%s', + resp.status_code, + resp.text, + ) + raise GenerationException from exc if image_data := data.get('data'): 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): + raise RequestBlocked + logger.error( + 'Bytedance images/generations status=%s response=%s', + resp.status_code, + resp.text, + ) raise GenerationException @@ -272,10 +315,22 @@ class BytedanceModelArkAdapter: resp = client.post(route, json=payload) try: data: BytedanceVideoTaskResponse = resp.json() - except Exception: - raise GenerationException + except Exception as exc: + logger.error( + 'Bytedance contents/generations/tasks create status=%s response=%s', + resp.status_code, + resp.text, + ) + raise GenerationException from exc if data.get('id') or data.get('task_id'): return data + if cls._is_request_blocked_error(data): + raise RequestBlocked + logger.error( + 'Bytedance contents/generations/tasks create status=%s response=%s', + resp.status_code, + resp.text, + ) raise GenerationException @@ -295,16 +350,32 @@ class BytedanceModelArkAdapter: resp = client.get(route) try: data: BytedanceVideoTaskResponse = resp.json() - except Exception: - raise GenerationException + except Exception as exc: + logger.error( + 'Bytedance contents/generations/tasks retrieve status=%s task_id=%s response=%s', + resp.status_code, + task_id, + resp.text, + ) + raise GenerationException from exc if data.get('id') or data.get('task_id'): return data + if cls._is_request_blocked_error(data): + raise RequestBlocked + logger.error( + 'Bytedance contents/generations/tasks retrieve status=%s task_id=%s response=%s', + resp.status_code, + task_id, + resp.text, + ) raise GenerationException @classmethod def _wait_video_task_result(cls, task_id: str) -> RunVideoResult: for _ in range(cls.VIDEO_POLL_ATTEMPTS_LIMIT): data = cls._retrieve_video_task(task_id=task_id) + if cls._is_request_blocked_error(data): + raise RequestBlocked status = str(data.get('status', '')).lower() if status == BytedanceVideoTaskStatus.SUCCEEDED: return data['content']['video_url'], data['usage']['completion_tokens'] @@ -7,6 +7,7 @@ from ml_model.services.dalle import Dalle from ml_model.services.deepl import Deepl from ml_model.services.deepseek import Deepseek from ml_model.services.djourney import Djourney +from ml_model.services.dola_seed import Dola_Seed from ml_model.services.elevenlabs import Elevenlabs from ml_model.services.elevenlabs_music import Elevenlabs_Music from ml_model.services.epicphotogasm import Epicphotogasm @@ -20,8 +21,8 @@ from ml_model.services.gemini import Gemini from ml_model.services.gemini_3_1 import Gemini_3_1 from ml_model.services.geminiimage import Geminiimage from ml_model.services.gemma import Gemma -from ml_model.services.gptimage import Gptimage from ml_model.services.glm_4_7 import Glm_4_7 +from ml_model.services.gptimage import Gptimage from ml_model.services.granite import Granite from ml_model.services.grok import Grok from ml_model.services.grok_4_1_fast import Grok_4_1_Fast @@ -0,0 +1,156 @@ +import time +import logging +from datetime import timedelta +from decimal import Decimal +from typing import Any, Iterator + +import filetype + +from messages.models import Message +from ml_model.adapters.bytedance_model_ark import BytedanceContentType, BytedanceModelArkAdapter +from ml_model.exceptions import FileExtensionNotSupported +from ml_model.services.FileService import FileProcessingService +from ml_model.services.base import SimpleService +from ml_model.tasks import bytedance_model_ark_run +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + +logger = logging.getLogger(__name__) + + +class Dola_Seed(SimpleService): + TOKENS_COST = { + 'seed-2-0-pro': { + 'short_prompt': { + 'input': Decimal('250'), + 'output': Decimal('1500'), + }, + 'long_prompt': { + 'input': Decimal('500'), + 'output': Decimal('3000'), + } + }, + 'seed-2-0-mini': { + 'short_prompt': { + 'input': Decimal('50'), + 'output': Decimal('200'), + }, + 'long_prompt': { + 'input': Decimal('100'), + 'output': Decimal('400'), + } + }, # 1M tokens + } + + VERSION_MAPPING = { + 'seed-2-0-pro': 'seed-2-0-pro-260328', + 'seed-2-0-mini': 'seed-2-0-mini-260215', + } + + 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" + price = ( + input_tokens * price_map[prompt_type]['input'] / 1_000_000 + + output_tokens * price_map[prompt_type]['output'] / 1_000_000 + ) + + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + msgs = [ + Message( + content=content, + content_object=self.store, + elapsed_time=t, + ) + ] + if save: + return Message.objects.bulk_create(msgs) + + return msgs + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + version = input_message.info.pop('version', 'seed-2-0-pro') + callback_data = { + "reasoning_effort": "minimal", + **input_message.info, + } + + messages = self.get_chat_history() + + content = [{'type': 'text', 'text': input_message.content}] + file = input_message.file or None + if file: + file_bytes = file.read() + kind = filetype.guess(file_bytes[:50]) + file_extension = ( + FileProcessingService.get_file_extension(kind.extension, file_bytes) if kind else None + ) + BytedanceModelArkAdapter.validate_user_attachment_extension(file_extension) + + if file_extension and file_extension.upper() == 'MP4': + content.append( + {'type': 'video_url', 'video_url': {'url': file.url}} + ) + else: + content.append( + {'type': 'image_url', 'image_url': {'url': file.url}} + ) + + messages.append({'role': 'user', 'content': content}) + + start_time = time.time() + + result = bytedance_model_ark_run( + model=self.VERSION_MAPPING[version], + callback_data=callback_data, + content_type=BytedanceContentType.CHAT, + messages=messages, + ) + + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + version=version, + input_tokens=result[1], + output_tokens=result[2], + ) + msgs = self.save_results(result[0], process_time, save) + + return msgs + + 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] + ) + ) + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] + ) + ) + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({'role': 'assistant', 'content': content}) + else: + memory.append({'role': 'user', 'content': content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + character_length -= len(memory.pop(0)['content']) + + return memory @@ -44,13 +44,18 @@ class Glm_4_7(SimpleService): return msgs def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() version = input_message.info.pop('version', 'glm-4-7-251222') - callback_data = {**input_message.info} + callback_data = { + "reasoning_effort": "minimal", + **input_message.info, + } messages = self.get_chat_history() messages.append({'role': 'user', 'content': input_message.content}) if input_message.file: logger.info('GLM file input ignored: model does not support image/video input') + + start_time = time.time() + result = bytedance_model_ark_run( model=version, callback_data=callback_data, @@ -5,24 +5,55 @@ from io import BytesIO from typing import Any import requests +from django.utils.translation import gettext as _ from django.core.files import File from replicate.exceptions import ModelError from messages.models import Message -from ml_model.exceptions import RequestBlocked, GenerationException +from ml_model.adapters.bytedance_model_ark import BytedanceContentType +from ml_model.exceptions import InvalidParameterError from ml_model.services.base import SimpleService -from ml_model.tasks import replicate_run +from ml_model.tasks import bytedance_model_ark_run +from payments.exceptions.insufficient_balance import InsufficientBalance +from payments.selectors.payment_plan_selector import PaymentPlanSelector class Seedream(SimpleService): - PRICE = Decimal('10.5') + TOKEN_COST = { + "seedream-boosted": { + "2K": Decimal('25'), + "3K": Decimal('50'), + "4K": Decimal('100'), + }, + "seedream-4.5": { + "2K": Decimal('25'), + "4K": Decimal('100'), + }, + } + + VERSION_MAPPING = { + "seedream-boosted": "seedream-5-0-260128", + "seedream-4.5": "seedream-4-5-251128", + } + + ENHANCEMENT_PROMPT = """[Speed-first — keep the user's idea] + Subject: one clear focal subject; same mood as the scene below. + Scene: simple background; avoid crowds, clutter, tiny props, dense patterns. + Light: one coherent light setup. No text, logos, or watermark in the image. + """ @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - return cls.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') + version = info.get('version', 'seedream-boosted') + size = info.get('size', '2K') + price = cls.TOKEN_COST[version][size] + + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self) -> Decimal: - return self.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, version: str, size: str) -> Decimal: + price = self.TOKEN_COST[version][size] + + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] @@ -41,24 +72,35 @@ class Seedream(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() - raw_aspect_ratio = input_message.info.pop('aspect_ratio', 'Исходное изображение') - aspect_ratio = ( - 'match_input_image' if raw_aspect_ratio == 'Исходное изображение' else raw_aspect_ratio - ) + version = input_message.info.pop('version', 'seedream-boosted') + size = input_message.info.get('size', '2K') + + if version == 'seedream-4.5' and size == '3K': + raise InvalidParameterError(_('3K output is not supported for Seedream 4.5')) + + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < (predicted := self.predict_price( + input_message.content, + input_message.file is not None, + input_message.info, + )): + raise InsufficientBalance(balance, predicted) + callback_data = { - 'prompt': input_message.content, - 'aspect_ratio': aspect_ratio, + 'prompt': f'{input_message.content}\n{self.ENHANCEMENT_PROMPT}', + 'watermark': False, **input_message.info, } if image := input_message.file: - callback_data.update({'image_input': [image.url]}) - try: - images = replicate_run('bytedance/seedream-5-lite', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + callback_data.update({'image': image.url}) + + images = bytedance_model_ark_run( + self.VERSION_MAPPING[version], + callback_data, + content_type=BytedanceContentType.IMAGE, + ) + process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model) + self.handle_invoice(input_message.content_object.model, version=version, size=size) msgs = self.save_results(input_message.content, images, process_time, save) + return msgs @@ -0,0 +1,63 @@ +""" +DEPRECATED вариант с запуском на Replicate +Хранится только для быстрого отката +""" + +import time +from datetime import timedelta +from decimal import Decimal +from io import BytesIO +from typing import Any + +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 + + +class Seedream(SimpleService): + PRICE = Decimal('10.5') + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def calculate_price(self) -> Decimal: + return self.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: + messages: list[Message] = [] + for image in images: + messages.append( + Message( + content_object=self.store, + elapsed_time=time, + content=prompt, + file=File(BytesIO(requests.get(image).content), '.png'), + ) + ) + if save: + return Message.objects.bulk_create(messages) + return messages + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + start_time = time.time() + raw_aspect_ratio = input_message.info.pop('aspect_ratio', 'Исходное изображение') + aspect_ratio = ( + 'match_input_image' if raw_aspect_ratio == 'Исходное изображение' else raw_aspect_ratio + ) + callback_data = { + 'prompt': input_message.content, + 'aspect_ratio': aspect_ratio, + **input_message.info, + } + if image := input_message.file: + callback_data.update({'image_input': [image.url]}) + images = replicate_run('bytedance/seedream-5-lite', callback_data) + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model) + msgs = self.save_results(input_message.content, images, process_time, save) + return msgs @@ -23,3 +23,5 @@ data/ celerybeat-schedule .idea/ + +scripts/