@@ -1512,6 +1512,9 @@ 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" + #~ msgid "Account is already confirmed" #~ msgstr "Аккаунт уже подтвержден" @@ -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,80 @@ 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', + } + EFFECTS = { + 'Без эффекта': 'None', + 'Танец YMCA': "Let's YMCA!", + 'Лихорадка Subject 3': 'Subject 3 Fever', + 'Гибли в жизни': 'Ghibli Live!', + 'Стильный свэг в костюме': 'Suit Swagger', + 'Мышечный буст': 'Muscle Surge', + 'Микроволновка 360°': '360° Microwave', + 'Тепло Иисуса': 'Warmth of Jesus', + 'Экстренный бит': 'Emergency Beat', + 'Что угодно, робот': 'Anything, Robot', + 'Клуб кунг-фу': 'Kungfu Club', + 'Мята в коробке': 'Mint in Box', + 'Ретро аниме поп': 'Retro Anime Pop', + 'Походка Vogue': 'Vogue Walk', + 'Мега-прыжок': 'Mega Dive', + 'Злой триггер': 'Evil Trigger', + } + 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 = info['motion_mode'] + sound_effect_switch = info['sound_effect_switch'] + price = cls.UNIT_PRICE * cls.PRICING_UNITS[quality][duration][motion_mode] + 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 +107,40 @@ 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) + units = self.PRICING_UNITS[quality][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'), + 'effect': self.EFFECTS.get(input_message.info.pop('effect', 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