@@ -680,6 +680,14 @@ msgstr "" msgid "The file may be corrupted. Please try another one." msgstr "Возможно, файл повреждён. Попробуйте загрузить другой файл." +#: ml_model/exceptions.py:65 +msgid "File Uploading Not supported" +msgstr "Загрузка файлов не поддерживается" + +#: ml_model/exceptions.py:65 +msgid "Unable to recognize the file" +msgstr "Не удаётся распознать файл" + #: ml_model/exceptions.py:73 msgid "The length of the context has been exceeded." msgstr "Длина контекста превышена." @@ -1512,6 +1520,16 @@ msgstr "Пресеты общие для всех — их нельзя удал #~ msgid "Issued achievement" #~ msgstr "Выданное достижение" +msgid "Smooth motion mode is available only for 5-second videos at 540p and 720p quality" +msgstr "Режим «Плавное движение» доступен только для 5-секундных видео в качестве 540p и 720p" + +#, python-format +msgid "This video duration is not allowed for %(quality)s quality." +msgstr "Для качества %(quality)s такая продолжительность видео не поддерживается." + +msgid "The model could not analyze your request. Please rephrase it and try again" +msgstr "Модель не смогла проанализировать ваш запрос. Перефразируйте его и попробуйте снова" + #~ msgid "Account is already confirmed" #~ msgstr "Аккаунт уже подтвержден" @@ -1,3 +1,4 @@ +import re import time from datetime import timedelta from uuid import uuid4 @@ -68,6 +69,11 @@ class Message(models.Model): def __str__(self) -> str: return f'Сообщение {self.pk}' + def save(self, *args, **kwargs): + if self.content: + self.content = re.sub(r'\x00', '', self.content) + super().save(*args, **kwargs) + class Meta: verbose_name = 'Сообщение' verbose_name_plural = 'Сообщения' @@ -16,7 +16,7 @@ router = Router(auth=AsyncAuthBearer(), tags=['ml_model']) @router.post('predict-price/', tags=['ml_model/predict-price'], response=PredictPriceSchema) def calculate_predict_price(request, body: PredictPriceInputSchema): payload = body.dict() - content = payload.pop('content') if body.model_slug != 'qwen_3_tts' else payload.get('content') + content = payload.pop('content') if body.model_slug != 'elevenlabs' else payload.get('content') json_str = json.dumps(payload, sort_keys=True, separators=(',', ':')) signature = hashlib.sha256(json_str.encode('utf-8')).hexdigest() cache_key = f'predict_price:{signature}' @@ -7,6 +7,8 @@ import openpyxl from io import BytesIO +from ml_model.exceptions import UnrecognizedFileError + class FileProcessingService: @classmethod @@ -18,7 +20,7 @@ class FileProcessingService: for format_name, required_file in signatures.items(): if required_file in namelist: return format_name - raise + raise UnrecognizedFileError return raw_file_extension @classmethod @@ -8,6 +8,7 @@ from ml_model.services.deepl import Deepl from ml_model.services.deepseek import Deepseek from ml_model.services.djourney import Djourney from ml_model.services.epicphotogasm import Epicphotogasm +from ml_model.services.elevenlabs_music import Elevenlabs_Music from ml_model.services.elevenlabs import Elevenlabs from ml_model.services.flux import Flux from ml_model.services.flux_2 import Flux_2 @@ -54,7 +55,6 @@ from ml_model.services.pruna_v import Pruna_V from ml_model.services.qwen import Qwen from ml_model.services.qwen_235B import Qwen_235B from ml_model.services.qwen_3_5 import Qwen_3_5 -from ml_model.services.qwen_3_tts import Qwen_3_Tts from ml_model.services.qwen_3_max_thinking import Qwen_3_Max_Thinking from ml_model.services.raifgpt import Raifgpt from ml_model.services.ray import Ray @@ -64,7 +64,7 @@ from ml_model.services.runway import Runway from ml_model.services.sdxlemoji import Sdxlemoji from ml_model.services.seedream import Seedream from ml_model.services.sora import Sora -from ml_model.services.speedance import Speedance +from ml_model.services.seedance import Seedance from ml_model.services.stablediffusion import Stablediffusion from ml_model.services.stablemusic import Stablemusic from ml_model.services.suno import Suno @@ -30,7 +30,11 @@ from PIL import Image from backend import settings from messages.models import BaseStore, Message from ml_model.constants import TEMPORARY_TEST_TEXT -from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError +from ml_model.exceptions import ( + CorruptedFileError, + FileExtensionNotSupported, + FileUploadUnsupported, +) from ml_model.models import ModelConfiguration, NeuronModel from ml_model.services.EmbeddingService import EmbeddingService from ml_model.services.FileService import FileProcessingService @@ -116,6 +120,8 @@ class Chatgpt(SimpleService): chunks = [] text_chunks: list[str] = [] if file: + if model_name == 'gpt-oss-120b': + raise FileUploadUnsupported file_service = FileProcessingService file_bytes = input_message.file.read() kind = filetype.guess(file_bytes[:20]) @@ -9,55 +9,57 @@ 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.exceptions import GenerationException, RequestBlocked from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run - from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector class Elevenlabs(SimpleService): - TOKENS_COST = Decimal('2.490') + TOKENS_PER_1K_CHARS = Decimal('6') @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - duration = info['duration'] - return (cls.TOKENS_COST * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') + chars = len(content) + price = cls.TOKENS_PER_1K_CHARS * Decimal(chars) / Decimal(1000) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, duration: int) -> Decimal: - return (self.TOKENS_COST * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, content: str) -> Decimal: + chars = len(content) + price = self.TOKENS_PER_1K_CHARS * Decimal(chars) / Decimal(1000) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, content: str, t: timedelta, video: str, save: bool = True) -> list[Message]: + def save_results(self, content: str, t: timedelta, audio_url: str, save: bool = True) -> list[Message]: msg = Message( content=content, content_object=self.store, elapsed_time=t, - file=File(BytesIO(requests.get(video).content), '.mp3'), + file=File(BytesIO(requests.get(audio_url).content), '.mp3'), ) if save: return Message.objects.bulk_create([msg]) return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - duration = input_message.info.pop('duration') - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( - cost := self.calculate_price(duration) - ): + cost = self.calculate_price(input_message.content) + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < cost: raise InsufficientBalance(balance, cost) callback_data = { - 'prompt': input_message.content, - 'music_length_ms': duration * 1000, - **input_message.info, + 'text': input_message.content, + 'mode': 'voice_clone', + 'reference_audio': input_message.file.url, } + if transcription := input_message.info.get('transcription', ''): + callback_data.update({'reference_text': transcription}) start_time = time.time() try: - audio = replicate_run('elevenlabs/music', callback_data) + result = replicate_run('qwen/qwen3-tts', callback_data) except ModelError as exc: if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): raise RequestBlocked raise GenerationException from exc + process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, duration=duration) - msgs = self.save_results(input_message.content, process_time, audio, save) - return msgs + self.handle_invoice(input_message.content_object.model, content=input_message.content) + return self.save_results(input_message.content, process_time, result, save) @@ -1,11 +1,9 @@ -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 replicate.exceptions import ModelError @@ -19,59 +17,47 @@ from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector -class Speedance(SimpleService): - - TOKENS_COST = { - '480p': Decimal('4.5'), - '720p': Decimal('7.5'), - '1080p': Decimal('18'), - } +class Elevenlabs_Music(SimpleService): + TOKENS_COST = Decimal('2.490') @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - resolution = info['resolution'] duration = info['duration'] - price = cls.TOKENS_COST[resolution] * duration - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + return (cls.TOKENS_COST * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, resolution: str, duration: int) -> Decimal: - price = self.TOKENS_COST[resolution] * duration - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, duration: int) -> Decimal: + return (self.TOKENS_COST * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, content: str, t: timedelta, video: str, save: bool = True) -> list[Message]: msg = Message( content=content, content_object=self.store, elapsed_time=t, - file=File(BytesIO(requests.get(video).content), '.mp4'), + file=File(BytesIO(requests.get(video).content), '.mp3'), ) if save: return Message.objects.bulk_create([msg]) return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - resolution = input_message.info.get('resolution', '720p') - duration = input_message.info.get('duration', 8) - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < (cost := self.TOKENS_COST[resolution] * duration): + duration = input_message.info.pop('duration') + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.calculate_price(duration) + ): raise InsufficientBalance(balance, cost) - callback_data = dict({'prompt': self.translate_prompt(input_message.content), **input_message.info}) - if input_message.file: - kind = filetype.guess(input_message.file.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - input_message.file.seek(0) - image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' - input_message.file.close() - callback_data.update({'image': image}) + callback_data = { + 'prompt': input_message.content, + 'music_length_ms': duration * 1000, + **input_message.info, + } start_time = time.time() try: - video = replicate_run( - f'bytedance/{input_message.info.get("version", "seedance-1-pro-fast")}', callback_data - ) + audio = replicate_run('elevenlabs/music', callback_data) except ModelError as exc: if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): raise RequestBlocked raise GenerationException from exc process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, resolution=resolution, duration=duration) - msgs = self.save_results(input_message.content, process_time, video, save) + self.handle_invoice(input_message.content_object.model, duration=duration) + msgs = self.save_results(input_message.content, process_time, audio, save) return msgs @@ -37,16 +37,14 @@ class Grok_4_1_Fast(SimpleService): return price.quantize(Decimal('0.01'), rounding='ROUND_UP') def save_results(self, content: str, t: timedelta, save: bool = True) -> list[Message]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] + msg = Message( + content=content, + content_object=self.store, + elapsed_time=t, + ) if save: - return Message.objects.bulk_create(msgs) - return msgs + msg.save() + return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: callback_data = { @@ -1,12 +1,16 @@ import time +import requests + 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 django.utils.translation import gettext as _ from messages.models import Message +from ml_model.exceptions import InvalidParameterError from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -15,13 +19,65 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Hunyuan(SimpleService): + UNIT_PRICE = Decimal('3') + MOTION_MODES = { + 'Плавное движение': 'smooth', + 'Обычное движение': 'normal', + } + STYLES = { + 'Без стиля': 'None', + 'Аниме': 'anime', + '3D анимация': '3d_animation', + 'Пластилин': 'clay', + 'Киберпанк': 'cyberpunk', + 'Комикс': 'comic', + } + PRICING_UNITS = { + '540p': { + 5: { + 'normal': 30, + 'smooth': 60, + }, + 8: { + 'normal': 60, + }, + }, + '720p': { + 5: { + 'normal': 40, + 'smooth': 80, + }, + 8: { + 'normal': 80, + }, + }, + '1080p': { + 5: { + 'normal': 80, + }, + }, + } - PRICE = Decimal('1.575') - - _CALLBACK = 'tencent/hunyuan-video:6c9132aee14409cd6568d030453f1ba50f5f3412b844fe67f78a9eb62d55664f' + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + quality = info['quality'] + duration = info['duration'] + motion_mode_ru = info['motion_mode'] + sound_effect_switch = info['sound_effect_switch'] + try: + price = cls.UNIT_PRICE * cls.PRICING_UNITS[quality][duration][cls.MOTION_MODES[motion_mode_ru]] + except KeyError: + return None + if sound_effect_switch: + price += 10 * cls.UNIT_PRICE + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, process_time: timedelta) -> Decimal: - price = self.PRICE * Decimal(process_time.total_seconds()) + def calculate_price( + self, quality: str, duration: int, motion_mode: str, sound_effect_switch: bool + ) -> Decimal: + price = self.UNIT_PRICE * self.PRICING_UNITS[quality][duration][motion_mode] + if sound_effect_switch: + price += 10 * self.UNIT_PRICE return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, content: str, t: timedelta, video: str, save: bool = True) -> list[Message]: @@ -36,18 +92,44 @@ class Hunyuan(SimpleService): return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - if ( - (balance := PaymentPlanSelector(self.store.user).get_current_balance()) - < (cost := Decimal('378')) + quality = input_message.info.get('quality') + duration = input_message.info.get('duration') + motion_mode_ru = input_message.info.pop('motion_mode') + motion_mode = self.MOTION_MODES.get(motion_mode_ru, 'normal') + sound_effect_switch = input_message.info.get('sound_effect_switch', False) + modes_for_duration = self.PRICING_UNITS[quality].get(duration) + if not modes_for_duration: + raise InvalidParameterError( + _('This video duration is not allowed for %(quality)s quality.') % {'quality': quality} + ) + units = modes_for_duration.get(motion_mode, {}) + if not units: + raise InvalidParameterError( + _('Smooth motion mode is available only for 5-second videos at 540p and 720p quality') + ) + elif sound_effect_switch: + units += 10 + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := units * self.UNIT_PRICE ): raise InsufficientBalance(balance, cost) callback_data = { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': input_message.content, + 'motion_mode': motion_mode, + 'style': self.STYLES.get(input_message.info.pop('style', None), 'None'), **input_message.info, } + if image := input_message.file: + callback_data.update({'image': image.url}) start_time = time.time() - video = replicate_run(self._CALLBACK, callback_data) + video = replicate_run('pixverse/pixverse-v4', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, process_time=process_time) + self.handle_invoice( + input_message.content_object.model, + quality=quality, + duration=duration, + motion_mode=motion_mode, + sound_effect_switch=sound_effect_switch, + ) msgs = self.save_results(input_message.content, process_time, video, save) return msgs @@ -11,7 +11,12 @@ from django.core.files import File from replicate.exceptions import ModelError from messages.models import Message -from ml_model.exceptions import ImageContentNotFound, GenerationException, RequestBlocked +from ml_model.exceptions import ( + ImageContentNotFound, + GenerationException, + ModelCouldNotInterpretPrompt, + RequestBlocked, +) from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -62,7 +67,7 @@ class Nanobanana(SimpleService): resolution = input_message.info.get('resolution', '2K') if version == 'nano-banana-pro' else None callback_data = dict( { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': input_message.content, **input_message.info, } ) @@ -78,6 +83,8 @@ class Nanobanana(SimpleService): except ModelError as exc: if exc.prediction.error == 'No image content found in response': raise ImageContentNotFound + elif exc.prediction.error in ('400', 'Failed to generate image.'): + raise ModelCouldNotInterpretPrompt from exc elif any(error in str(exc) for error in ('E005', 'E006', 'sexual')): raise RequestBlocked raise GenerationException from exc @@ -28,12 +28,12 @@ class Pixverse(SimpleService): @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: duration = info['duration'] - resolution = info['resolution'] - price = cls.TOKENS_COST[resolution] * duration + quality = info['quality'] + price = cls.TOKENS_COST[quality] * duration return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, resolution: str, duration: int) -> Decimal: - return (self.TOKENS_COST[resolution] * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, quality: str, duration: int) -> Decimal: + return (self.TOKENS_COST[quality] * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, content: str, t: timedelta, video: str, save: bool = True) -> list[Message]: msg = Message( @@ -48,15 +48,15 @@ class Pixverse(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: duration = input_message.info.get('duration', 5) - resolution = input_message.info.pop('resolution', '1080p') + quality = input_message.info.pop('quality', '1080p') if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( - cost := self.TOKENS_COST[resolution] * duration + cost := self.TOKENS_COST[quality] * duration ): raise InsufficientBalance(balance, cost) thinking_types = {'авто': 'auto', 'выкл.': 'disabled', 'вкл.': 'enabled'} callback_data = { 'prompt': self.translate_prompt(input_message.content), - 'quality': resolution, + 'quality': quality, 'thinking_type': thinking_types[input_message.info.pop('thinking_type', 'авто').lower()], **input_message.info, } @@ -70,6 +70,6 @@ class Pixverse(SimpleService): raise RequestBlocked raise GenerationException from exc process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, resolution=resolution, duration=duration) + self.handle_invoice(input_message.content_object.model, quality=quality, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) return msgs @@ -1,65 +1,108 @@ +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 replicate.exceptions import ModelError from messages.models import Message -from ml_model.exceptions import GenerationException, RequestBlocked +from ml_model.exceptions import RequestBlocked, GenerationException, FileExtensionNotSupported +from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run + from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector -class Qwen_3_Tts(SimpleService): - TOKENS_PER_1K_CHARS = Decimal('6') +class Seedance(SimpleService): + TOKENS_COST = { + 'seedance-2.0': { + 'non_video_in': { + '480p': Decimal('21'), + '720p': Decimal('51') + } + }, + 'seedance-2.0-fast': { + 'non_video_in': { + '480p': Decimal('18'), + '720p': Decimal('39') + }, + 'video_in': { + '480p': Decimal('33'), + '720p': Decimal('66') + } # 1 second + } + } @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - chars = len(content) - price = cls.TOKENS_PER_1K_CHARS * Decimal(chars) / Decimal(1000) + resolution = info['resolution'] + duration = info['duration'] + version = info['version'] + generation_type = 'video_in' if file_exists and version == 'seedance-2.0-fast' else 'non_video_in' + price = cls.TOKENS_COST[version][generation_type][resolution] * duration return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, content: str) -> Decimal: - chars = len(content) - price = self.TOKENS_PER_1K_CHARS * Decimal(chars) / Decimal(1000) + def calculate_price(self, resolution: str, duration: int, version: str, generation_type: str) -> Decimal: + price = self.TOKENS_COST[version][generation_type][resolution] * duration return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, content: str, t: timedelta, audio_url: str, save: bool = True) -> list[Message]: + def save_results(self, content: str, t: timedelta, video: str, save: bool = True) -> list[Message]: msg = Message( content=content, content_object=self.store, elapsed_time=t, - file=File(BytesIO(requests.get(audio_url).content), '.mp3'), + file=File(BytesIO(requests.get(video).content), '.mp4'), ) if save: return Message.objects.bulk_create([msg]) return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - cost = self.calculate_price(input_message.content) - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < cost: + version = input_message.info.pop('version', 'seedance-2.0') + resolution = input_message.info.get('resolution', '720p') + duration = input_message.info.get('duration', 5) + file = input_message.file or None + file_extension = None + generation_type = 'non_video_in' + 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 + available_extensions = ('JPG', 'JPEG', 'PNG', 'WEBP', 'MP4') + if not file_extension or file_extension.upper() not in available_extensions: + raise FileExtensionNotSupported(available_extensions) + elif file_extension.upper() == 'MP4': + generation_type = 'video_in' + if version == 'seedance-2.0' and generation_type == 'video_in': + available_extensions = ('JPG', 'JPEG', 'PNG', 'WEBP') + raise FileExtensionNotSupported(available_extensions) + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.TOKENS_COST[version][generation_type][resolution] * duration): raise InsufficientBalance(balance, cost) - callback_data = { - 'text': input_message.content, - 'mode': 'voice_clone', - 'reference_audio': input_message.file.url, - } - if transcription := input_message.info.get('transcription', ''): - callback_data.update({'reference_text': transcription}) + callback_data = dict({'prompt': input_message.content, **input_message.info}) + if file: + reference_type = ( + 'videos' if file_extension.upper() == 'MP4' + else 'images' + ) + callback_data.update({f'reference_{reference_type}': [file.url]}) start_time = time.time() try: - result = replicate_run('qwen/qwen3-tts', callback_data) + video = replicate_run( + f'bytedance/{version}', callback_data + ) except ModelError as exc: if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): raise RequestBlocked raise GenerationException from exc - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, content=input_message.content) - return self.save_results(input_message.content, process_time, result, save) + self.handle_invoice(input_message.content_object.model, resolution=resolution, duration=duration, version=version, generation_type=generation_type) + msgs = self.save_results(input_message.content, process_time, video, save) + return msgs @@ -1,11 +1,9 @@ -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 replicate.exceptions import ModelError @@ -17,15 +15,14 @@ from ml_model.tasks import replicate_run class Seedream(SimpleService): - PRICE = Decimal('9') + PRICE = Decimal('10.5') @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - max_images = 5 if info['story_mode'] else 1 - return cls.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') * max_images + return cls.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, max_images: int) -> Decimal: - return self.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') * max_images + 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] = [] @@ -44,28 +41,24 @@ 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 + ) callback_data = { - 'prompt': self.translate_prompt(input_message.content), - 'size': 'custom', + 'prompt': input_message.content, + 'aspect_ratio': aspect_ratio, **input_message.info, } - if input_message.info.get('story_mode', False): - callback_data.update({'sequential_image_generation': 'auto', 'max_images': 5}) - callback_data['prompt'] = f'Generate a sequence of multiple images. {callback_data["prompt"]}' - if input_message.file: - kind = filetype.guess(input_message.file.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - input_message.file.seek(0) - image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' - input_message.file.close() - callback_data.update({'image_input': [image]}) + if image := input_message.file: + callback_data.update({'image_input': [image.url]}) try: - images = replicate_run('bytedance/seedream-4', callback_data) + 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 process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, max_images=len(images)) + self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, images, process_time, save) return msgs @@ -8,9 +8,10 @@ from typing import Any import filetype import requests from django.core.files import File +from replicate.exceptions import ModelError from messages.models import Message -from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError +from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError, GenerationException from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -63,7 +64,12 @@ class Wan_Lite(SimpleService): input_message.file.close() callback_data.update({'image': image}) start_time = time.time() - video = replicate_run('wan-video/wan-2.2-5b-fast', callback_data) + try: + video = replicate_run('wan-video/wan-2.2-5b-fast', callback_data) + except ModelError as exc: + if 'image file' in exc.prediction.error: + raise CorruptedFileError from exc + raise GenerationException from exc process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, resolution=resolution) msgs = self.save_results(input_message.content, process_time, video, save) @@ -60,6 +60,16 @@ class CorruptedFileError(Exception): return _('The file may be corrupted. Please try another one.') +class FileUploadUnsupported(Exception): + def __str__(self) -> str: + return _('File Uploading Not supported') + + +class UnrecognizedFileError(Exception): + def __str__(self) -> str: + return _('Unable to recognize the file') + + class FileTooLargeError(Exception): def __init__(self, max_mb_size: int) -> None: self.max_mb_size = max_mb_size @@ -101,6 +111,11 @@ class ImageContentNotFound(Exception): return _('No image content found in response. Try a different request') +class ModelCouldNotInterpretPrompt(Exception): + def __str__(self): + return _('The model could not analyze your request. Please rephrase it and try again') + + class ImageAnalysisError(Exception): def __str__(self): return _('Image analysis error. Please try another image.') @@ -226,6 +226,7 @@ class ModelInput(ModelDepends, ModelVersionsDepends): TXT = 'txt', _('Text File (Notebook)') ZIPARCHIVE = 'zip', _('ZIP Archive') AUDIO = 'audio', _('Audio') + VIDEO = 'video', _('Video') type = models.CharField( max_length=32, @@ -25,7 +25,9 @@ from ml_model.exceptions import ( ExceededContextLengthError, FileExtensionNotSupported, FileTooLargeError, + FileUploadUnsupported, ImageAnalysisError, + UnrecognizedFileError, PaidPlanRequiredError, PromptLengthExceeded, RequestBlocked, @@ -178,6 +180,8 @@ class MessagesAPIView(APIView): CorruptedFileError, FileTooLargeError, ImageAnalysisError, + FileUploadUnsupported, + UnrecognizedFileError, ) as exc: return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) except TemplateNotFound as exc: @@ -16,9 +16,11 @@ from ml_model.exceptions import ( FileExtensionNotSupported, FileNotProvided, FileTooLargeError, + UnrecognizedFileError, ImageAnalysisError, ImageContentNotFound, InvalidParameterError, + ModelCouldNotInterpretPrompt, InvalidStyleCombinationError, PromptLengthExceeded, RequestBlocked, @@ -183,6 +185,7 @@ class MediaAPIView(APIView): RequestBlocked, FileNotProvided, ImageContentNotFound, + ModelCouldNotInterpretPrompt, InvalidStyleCombinationError, InvalidParameterError, PromptLengthExceeded, @@ -192,6 +195,7 @@ class MediaAPIView(APIView): CorruptedFileError, FileTooLargeError, ImageAnalysisError, + UnrecognizedFileError, ), ): return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST)