@@ -25,6 +25,7 @@ api.add_router('media/', 'tools.media.routes.v1.router') compatibility_api.add_router('auth/', 'authentication.routes.v1.router') compatibility_api.add_router('payments/', 'payments.routes.v1.router') compatibility_api.add_router('reports/', 'reports.routes.v1.router') +compatibility_api.add_router('ml_model/', 'ml_model.routes.v1.router') logger = logging.getLogger(__name__) @@ -0,0 +1,37 @@ +import hashlib +import json +import sys +from decimal import Decimal + +from django.core.cache import cache +from ninja import Router + +from authentication.security import AsyncAuthBearer +from ml_model.schemas import PredictPriceSchema, PredictPriceInputSchema +from ml_model.services.base import SimpleService + +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') + json_str = json.dumps(payload, sort_keys=True, separators=(',', ':')) + signature = hashlib.sha256(json_str.encode('utf-8')).hexdigest() + cache_key = f'predict_price:{signature}' + predicted_price = cache.get(cache_key) + + if predicted_price is None: + service: type[SimpleService] = getattr( + sys.modules['ml_model.services'], f'{body.model_slug.title()}' + ) + predicted_price = service.predict_price(content=content, file_exists=body.file_exists, info=body.info) + if predicted_price: + cache.set(cache_key, predicted_price) + + if predicted_price: + predicted_price = predicted_price.quantize(Decimal('0.01')) + + return PredictPriceSchema(price=predicted_price) + @@ -21,6 +21,7 @@ from ml_model.services.hailuo import Hailuo from ml_model.services.hunyuan import Hunyuan from ml_model.services.iconic import Iconic from ml_model.services.ideogram import Ideogram +from ml_model.services.imagen import Imagen from ml_model.services.kandinsky import Kandinsky from ml_model.services.kling import Kling from ml_model.services.leonardo import Leonardo @@ -31,11 +32,13 @@ from ml_model.services.lyria import Lyria from ml_model.services.midjourney import Midjourney from ml_model.services.minimaxvideo import Minimaxvideo from ml_model.services.minimaxmusic import Minimaxmusic +from ml_model.services.minimaxmusic_lite import Minimaxmusic_Lite from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen from ml_model.services.nanobanana import Nanobanana from ml_model.services.perplexity import Perplexity from ml_model.services.pulid import Pulid +from ml_model.services.photon import Photon from ml_model.services.qwen import Qwen from ml_model.services.qwen_235B import Qwen_235B from ml_model.services.qwen_3_max_thinking import Qwen_3_Max_Thinking @@ -55,4 +58,5 @@ from ml_model.services.upscaleai import Upscaleai from ml_model.services.veo import Veo from ml_model.services.vicuna import Vicuna from ml_model.services.wan import Wan +from ml_model.services.wan_lite import Wan_Lite from ml_model.services.whisper import Whisper @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from decimal import Decimal -from typing import Never +from typing import Never, Any from asgiref.sync import async_to_sync from googletrans import Translator @@ -78,3 +78,7 @@ class SimpleService(ABC): @abstractmethod def make(self, input_message: Message, save: bool = True) -> list[Message]: ... + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return None @@ -2,6 +2,7 @@ import time from _decimal import Decimal from datetime import timedelta from io import BytesIO +from typing import Any import requests from django.core.files import File @@ -25,6 +26,10 @@ class Dalle(SimpleService): 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' ) + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return info.get('num_outputs', 1) * cls.PRICE + def calculate_price(self, input_message: Message) -> Decimal: price = input_message.info.get('num_outputs', 1) * self.PRICE return price.quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -2,6 +2,7 @@ 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 @@ -34,6 +35,11 @@ class Flux(SimpleService): price = price * image_count return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = cls.TOKENS_COST['flux-schnell']['input_imgs'] * info.get('num_outputs', 1) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + _CALLBACK_BASE = 'black-forest-labs/' @property @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -35,6 +36,11 @@ class Fluxkrea(SimpleService): price = price * image_count return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = cls.TOKENS_COST['flux-krea-dev']['input_imgs'] * info.get('num_outputs', 1) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + _CALLBACK_BASE = 'black-forest-labs/' @property @@ -29,6 +29,11 @@ class Fluxlorafast(SimpleService): price = price * image_count return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = cls.TOKENS_COST['flux-lora']['input_imgs'] * info.get('num_outputs', 1) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def save_results( self, prompt: str, @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -42,6 +43,12 @@ class Fluxproultra(SimpleService): price = price * image_count return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + price = cls.TOKENS_COST[version]['input_imgs'] * info.get('num_outputs', 1) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), ModelInput(type=ModelInput.TypeChoices.IMAGE), @@ -56,6 +56,11 @@ class Gemini(SimpleService): 'input_imgs': Decimal('0'), 'highest_prices': {'input': Decimal('1200'), 'output': Decimal('5400')}, }, + 'gemini-3-flash-preview': { + 'input': Decimal('150'), + 'output': Decimal('900'), + 'input_imgs': Decimal('0'), + }, } TOOLS_TOKEN_COSTS = {'text-embedding-3-large': {'output': Decimal('0.000065')}} @@ -77,7 +82,6 @@ class Gemini(SimpleService): if image: price += price_map['input_imgs'] / 1_000 if embedding_tokens > 0: - print(embedding_tokens) price += self.TOOLS_TOKEN_COSTS['text-embedding-3-large']['output'] * embedding_tokens return price.quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -102,6 +106,38 @@ class Gemini(SimpleService): } messages = self.get_chat_history() messages.append({'role': 'user', 'content': input_message.content}) + if version == 'google/gemini-3-flash-preview': + messages.insert( + 0, + { + 'role': 'system', + 'content': 'You are operating in Deep Analytical Reasoning Mode. Your goal is to approximate ' + 'research-grade reasoning depth similar to advanced long-thinking models while ' + 'maintaining accuracy, structure, and verification. CORE DIRECTIVES: 1. Decompose ' + 'every complex problem before answering — identify knowns, unknowns, constraints, ' + 'and assumptions; break tasks into sub-problems. 2. Use Multi-Hypothesis Reasoning — ' + 'generate multiple solution paths and explore 2–3 strategies when complexity is high. ' + '3. Apply Step-by-Step Logical Derivation — show intermediate reasoning and justify ' + 'each transition logically or mathematically. 4. Perform Cross-Validation — re-check ' + 'conclusions using alternative logic, formulas, or perspectives and detect ' + 'contradictions. 5. Run an Error Detection Loop — reassess derived answers, ' + 'question possible mistakes, and revise if needed. 6. Evidence-Bound Reasoning Only — ' + 'base conclusions strictly on provided data or established knowledge; state uncertainty ' + 'explicitly. DEPTH SCALING: Automatically increase reasoning depth for mathematics, ' + 'algorithms, system design, scientific analysis, financial modeling, legal reasoning, ' + 'and architecture planning. STRUCTURED OUTPUT FORMAT for complex tasks: Problem ' + 'Decomposition → Variables & Constraints → Hypothesis Generation → Step-by-Step ' + 'Reasoning → Cross-Validation → Final Answer → Confidence Level with justification. ' + 'ANTI-SHALLOW RULES: Do not skip reasoning steps, avoid surface-level summaries, ' + 'avoid intuition-only answers, prefer rigor over brevity. SELF-REFLECTION DIRECTIVE: ' + 'Review the reasoning chain before finalizing, identify gaps, and strengthen weak logic. ' + 'Priority: analytical depth, internal consistency, and correctness over speed.', + }, + ) + callback_data.update({ + "reasoning": {"effort": "high"}, + "temperature": 0.2 + }) file = input_message.file image = None embedding_tokens = 0 @@ -2,6 +2,7 @@ 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 @@ -25,6 +26,11 @@ class Geminiimage(SimpleService): price = num_images * self.TOKENS_COST return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = info.get('num_images', 1) * cls.TOKENS_COST + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def save_results(self, content: str, t: timedelta, image_url: str, save: bool = True) -> list[Message]: msg = Message( content=content, @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -30,10 +31,17 @@ class Hailuo(SimpleService): } } - def calculate_price(self, version: str, resolution: str) -> Decimal: + def calculate_price(self, version: str, resolution: str) -> Decimal: price = self.TOKENS_COST[version][resolution] return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + resolution = info['resolution'] + price = cls.TOKENS_COST[version][resolution] + return price.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, @@ -2,6 +2,7 @@ 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 @@ -35,6 +36,11 @@ class Ideogram(SimpleService): price = price_map['input_imgs'] return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = cls.TOKENS_COST['ideogram-v3-turbo']['input_imgs'] + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @property def neuron_model(self): return NeuronModel.objects.get(title='Flux') @@ -0,0 +1,62 @@ +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 replicate.exceptions import ModelError + +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 +from payments.selectors.payment_plan_selector import PaymentPlanSelector + + +class Imagen(SimpleService): + TOKENS_COST = Decimal('5') + + def calculate_price(self) -> Decimal: + return self.TOKENS_COST + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST + + def save_results(self, content: str, t: timedelta, image_url: str, save: bool = True) -> list[Message]: + msg = Message( + content=content, + content_object=self.store, + elapsed_time=t, + file=File(BytesIO(requests.get(image_url).content), '.png'), + ) + if save: + return Message.objects.bulk_create([msg]) + return [msg] + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + try: + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: + raise InsufficientBalance(balance, self.TOKENS_COST) + callback_data = dict( + { + 'prompt': self.translate_prompt(input_message.content), + 'safety_filter_level': 'block_medium_and_above', + **input_message.info, + } + ) + start_time = time.time() + images = replicate_run('google/imagen-3-fast', 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, process_time, images, save) + return msgs + except ModelError as exc: + if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): + raise RequestBlocked + elif exc.prediction.error == 'No image content found in response': + raise ImageContentNotFound + raise GenerationException @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -11,7 +12,7 @@ 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 RequestBlocked, GenerationException, FileNotProvided from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -20,10 +21,14 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Kling(SimpleService): - TOKENS_COST = { - 'standard': Decimal('15'), - 'pro': Decimal('27') - } + TOKENS_COST = {'standard': Decimal('15'), 'pro': Decimal('27')} + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + mode = info['mode'] + duration = info['duration'] + price = cls.TOKENS_COST[mode] * duration + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def calculate_price(self, mode: str, duration: int) -> Decimal: price = self.TOKENS_COST[mode] * duration @@ -45,14 +50,15 @@ class Kling(SimpleService): duration = input_message.info.get('duration', 5) if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < (cost := self.TOKENS_COST[mode] * duration): raise InsufficientBalance(balance, cost) + if not input_message.file: + raise FileNotProvided('Image') callback_data = dict({'prompt': self.translate_prompt(input_message.content), 'mode': mode, **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({'start_image': image}) + 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({'start_image': image}) start_time = time.time() try: video = replicate_run('kwaivgi/kling-v2.1', callback_data) @@ -3,6 +3,7 @@ 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 @@ -38,6 +39,11 @@ class Leonardo(SimpleService): price = price * num_images return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = cls.TOKENS_COST['lucid-origin']['input_imgs'] / 1_000 * info['num_images'] + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @property def neuron_model(self): return NeuronModel.objects.get(title='Flux') @@ -2,6 +2,7 @@ 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 @@ -16,7 +17,13 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Lyria(SimpleService): - TOKENS_COST = Decimal('0.6') # per 1 sec of output audio + TOKENS_COST = Decimal('0.6') # per 1 sec of output audio + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + duration = info.get('duration', 32) + price = cls.TOKENS_COST * duration + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def calculate_price(self, duration: int) -> Decimal: price = self.TOKENS_COST * duration @@ -2,6 +2,7 @@ import time from _decimal import Decimal from datetime import timedelta from io import BytesIO +from typing import Any import requests from django.core.files import File @@ -27,6 +28,11 @@ class Midjourney(SimpleService): price = input_message.info.get('number_of_images', 1) * self.price return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + price = info['number_of_images'] * cls.price + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def save_results( self, input_prompt: str, r: list[str], t: timedelta, save: bool = True ) -> list[Message]: @@ -2,6 +2,7 @@ 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 @@ -15,11 +16,14 @@ from ml_model.tasks import replicate_run from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector -from tools.media.models import Preset class Minimaxmusic(SimpleService): - TOKENS_COST = Decimal('10.5') + TOKENS_COST = Decimal('9') + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST def calculate_price(self) -> Decimal: return self.TOKENS_COST @@ -36,22 +40,19 @@ class Minimaxmusic(SimpleService): return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - speaker = input_message.info.get('speaker', 'russian_1').lower() - instrumental = input_message.info.get('instrumental', 'classical').lower() if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: raise InsufficientBalance(balance, self.TOKENS_COST) - callback_data = {'lyrics': input_message.content, **input_message.info} - if speaker: - file_url = Preset.objects.get(slug=speaker).file.url - callback_data.update({'voice_file': file_url}) - if input_message.file: - callback_data.update({'instrumental_file': input_message.file.url}) - else: - file_url = Preset.objects.get(slug=instrumental).file.url - callback_data.update({'instrumental_file': file_url}) + callback_data = { + 'prompt': f'High-quality professional music production, rich instrumentation, detailed arrangement, ' + f'studio-quality mixing and mastering, wide stereo imaging, clear vocals, emotional ' + f'performance, dynamic progression, polished sound design, immersive atmosphere ' + f'for {input_message.info.pop("style")}', + 'lyrics': input_message.content, + **input_message.info, + } start_time = time.time() try: - audio = replicate_run(f'minimax/music-01', callback_data) + audio = replicate_run('minimax/music-1.5', callback_data) except ModelError as exc: if 'lyrics is too long' in str(exc): raise InvalidParameterError(_('Lyrics is too long')) @@ -0,0 +1,69 @@ +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 django.utils.translation import gettext as _ +from replicate.exceptions import ModelError + +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 + +from payments.exceptions.insufficient_balance import InsufficientBalance +from payments.selectors.payment_plan_selector import PaymentPlanSelector +from tools.media.models import Preset + + +class Minimaxmusic_Lite(SimpleService): + TOKENS_COST = Decimal('7') + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST + + def calculate_price(self) -> Decimal: + return self.TOKENS_COST + + 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), '.mp3'), + ) + if save: + return Message.objects.bulk_create([msg]) + return [msg] + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + speaker = input_message.info.get('speaker', 'russian_1').lower() + instrumental = input_message.info.get('instrumental', 'classical').lower() + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: + raise InsufficientBalance(balance, self.TOKENS_COST) + callback_data = {'lyrics': input_message.content, **input_message.info} + if speaker: + file_url = Preset.objects.get(slug=speaker).file.url + callback_data.update({'voice_file': file_url}) + if input_message.file: + callback_data.update({'instrumental_file': input_message.file.url}) + else: + file_url = Preset.objects.get(slug=instrumental).file.url + callback_data.update({'instrumental_file': file_url}) + start_time = time.time() + try: + audio = replicate_run('minimax/music-01', callback_data) + except ModelError as exc: + if 'lyrics is too long' in str(exc): + raise InvalidParameterError(_('Lyrics is too long')) + 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) + msgs = self.save_results(input_message.content, process_time, audio, save) + return msgs @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -23,6 +24,10 @@ class Minimaxvideo(SimpleService): 'video-01': Decimal('150'), } + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST['video-01'] + def calculate_price(self, version: str) -> Decimal: return self.TOKENS_COST[version] @@ -3,7 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO -from typing import Optional +from typing import Optional, Any import filetype import requests @@ -31,6 +31,14 @@ class Nanobanana(SimpleService): return self.TOKENS_COST[version][resolution] return self.TOKENS_COST[version] + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + resolution = info['resolution'] if version == 'nano-banana-pro' else None + if resolution: + return cls.TOKENS_COST[version][resolution] + return cls.TOKENS_COST[version] + def save_results( self, prompt: str, @@ -0,0 +1,72 @@ +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 ImageContentNotFound, 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 Photon(SimpleService): + TOKENS_COST = Decimal('2') + + def calculate_price(self) -> Decimal: + return self.TOKENS_COST + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST + + def save_results(self, content: str, t: timedelta, image_url: str, save: bool = True) -> list[Message]: + msg = Message( + content=content, + content_object=self.store, + elapsed_time=t, + file=File(BytesIO(requests.get(image_url).content), '.png'), + ) + if save: + return Message.objects.bulk_create([msg]) + return [msg] + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: + raise InsufficientBalance(balance, self.TOKENS_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_reference': image}) + start_time = time.time() + try: + images = replicate_run('luma/photon-flash', callback_data) + except ModelError as exc: + if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): + raise RequestBlocked + elif exc.prediction.error == 'No image content found in response': + raise ImageContentNotFound + raise GenerationException + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model) + msgs = self.save_results(input_message.content, process_time, images, save) + return msgs @@ -13,15 +13,15 @@ from tools.public_api.models import APIStore class Qwen_3_Max_Thinking(SimpleService): TOKENS_COST = { 'input': {'default': Decimal('360'), 'high': Decimal('900')}, - 'output': {'default': Decimal('1800'), 'high': Decimal('4500')} + 'output': {'default': Decimal('1800'), 'high': Decimal('4500')}, } def calculate_price(self, input_tokens: int, output_tokens: int) -> Decimal: - price = ( - input_tokens * (self.TOKENS_COST['input']['default' if input_tokens <= 128_000 else 'high'] / 1_000_000) - + output_tokens * (self.TOKENS_COST['output']['default' if input_tokens <= 128_000 else 'high'] / 1_000_000) - + Decimal('6') - ) + 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]: @@ -41,7 +41,7 @@ class Qwen_3_Max_Thinking(SimpleService): callback_data = {'provider': {'order': ['alibaba']}, **input_message.info} messages = self.get_chat_history() messages.append({'role': 'user', 'content': input_message.content}) - result = openrouter_run('qwen/qwen3-max:online', messages, callback_data, 'Qwen') + result = openrouter_run('qwen/qwen3-max-thinking:online', messages, callback_data, 'Qwen') process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, @@ -51,13 +51,15 @@ class Qwen_3_Max_Thinking(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): @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -21,6 +22,13 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Ray(SimpleService): TOKENS_COST = {'ray-2-720p': Decimal('54'), 'ray-flash-2-540p': Decimal('9.9')} + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + duration = info['duration'] + version = info['version'] + price = cls.TOKENS_COST[version] * duration + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, version: str, duration: int) -> Decimal: return (self.TOKENS_COST[version] * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -2,6 +2,7 @@ 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 @@ -124,6 +125,11 @@ class Recraft(SimpleService): def calculate_price(self, input_message: Message) -> Decimal: return self.payment_rules[input_message.info.get('version', 'recraft-v3')] + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + return cls.payment_rules.get(version) + def save_results( self, prompt: str, @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -19,6 +20,11 @@ class Reve(SimpleService): 'edit-fast': Decimal('3') } + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + type_ = 'edit-fast' if file_exists else 'create' + return cls.PRICE[type_].quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, type: str) -> Decimal: return self.PRICE[type].quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -24,6 +25,13 @@ class Runway(SimpleService): 'gen4-turbo': Decimal('15'), } + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + duration = info['duration'] + price = cls.TOKENS_COST[version] * duration + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, version: str, duration: int) -> Decimal: price = self.TOKENS_COST[version] * duration return price.quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -18,6 +19,11 @@ from ml_model.tasks import replicate_run class Seedream(SimpleService): PRICE = Decimal('9') + @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 + def calculate_price(self, max_images: int) -> Decimal: return self.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') * max_images @@ -4,6 +4,7 @@ import filetype from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any from PIL import Image import httpx @@ -28,6 +29,13 @@ class Sora(SimpleService): price = Decimal(seconds) * self.TOKENS_COST[version] return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + seconds = int(info['seconds']) + price = Decimal(seconds) * cls.TOKENS_COST[version] + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def save_results(self, content: str, t: timedelta, video: bytes, save: bool = True) -> list[Message]: msg = Message( content=content, @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -26,6 +27,13 @@ class Speedance(SimpleService): '1080p': Decimal('18'), } + @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') + def calculate_price(self, resolution: str, duration: int) -> Decimal: price = self.TOKENS_COST[resolution] * duration return price.quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -4,6 +4,7 @@ import uuid from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import httpx from django.conf import settings @@ -38,6 +39,17 @@ class Stablediffusion(SimpleService): elif input_message.info.get('version') == 'sd3-medium': return Decimal('17.5') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info.get('version') + if version == 'sd3': + return Decimal('32.5') + elif version == 'sd3-turbo': + return Decimal('20') + elif version == 'sd3-medium': + return Decimal('17.5') + return None + def save_results( self, input_prompt: str, @@ -2,6 +2,7 @@ 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 @@ -16,6 +17,10 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Stablemusic(SimpleService): PRICE = Decimal('80') + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.PRICE + def calculate_price(self) -> Decimal: return self.PRICE @@ -3,6 +3,7 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO +from typing import Any import filetype import requests @@ -21,6 +22,11 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Veo(SimpleService): TOKENS_COST = {'veo-3': Decimal('640'), 'veo-3-fast': Decimal('240')} + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + version = info['version'] + return cls.TOKENS_COST[version] + def calculate_price(self, version: str) -> Decimal: return self.TOKENS_COST[version] @@ -1,12 +1,16 @@ +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 FileNotProvided from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -15,18 +19,16 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Wan(SimpleService): - """ - Wan Service - contains abstract method make, which makes a generation - """ + TOKENS_COST = {'720p': Decimal('25'), '1080p': Decimal('37.5')} - TOKENS_COST = { - '480p': Decimal('25'), - '720p': Decimal('50') - } + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + resolution = info['resolution'] + duration = info['duration'] + return (cls.TOKENS_COST[resolution] * duration).quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self, resolution: str) -> Decimal: - return self.TOKENS_COST[resolution].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 save_results(self, content: str, t: timedelta, video: str, save: bool = True) -> list[Message]: msg = Message( @@ -41,12 +43,30 @@ class Wan(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]: - raise InsufficientBalance(balance, self.TOKENS_COST[resolution]) - callback_data = dict({'prompt': self.translate_prompt(input_message.content), 'resolution': resolution, **input_message.info}) + duration = input_message.info.pop('duration', 5) + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.TOKENS_COST[resolution] * duration + ): + raise InsufficientBalance(balance, cost) + if not input_message.file: + raise FileNotProvided('Image') + 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 = dict( + { + 'prompt': self.translate_prompt(input_message.content), + 'image': image, + 'resolution': resolution, + 'duration': duration, + **input_message.info, + } + ) start_time = time.time() - video = replicate_run('wan-video/wan-2.2-t2v-fast', callback_data) + video = replicate_run('wan-video/wan2.6-i2v-flash', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, resolution) + self.handle_invoice(input_message.content_object.model, resolution=resolution, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) return msgs @@ -0,0 +1,65 @@ +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.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 Wan_Lite(SimpleService): + TOKENS_COST = {'480p': Decimal('2.5'), '720p': Decimal('5')} + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + resolution = info['resolution'] + return cls.TOKENS_COST[resolution].quantize(Decimal('0.1'), rounding='ROUND_UP') + + def calculate_price(self, resolution: str) -> Decimal: + return self.TOKENS_COST[resolution].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'), + ) + 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.pop('resolution', '720p') + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST[resolution]: + raise InsufficientBalance(balance, self.TOKENS_COST[resolution]) + callback_data = dict( + { + 'prompt': self.translate_prompt(input_message.content), + 'resolution': resolution, + **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}) + start_time = time.time() + video = replicate_run('wan-video/wan-2.2-5b-fast', callback_data) + 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) + return msgs @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import List, Optional, Any -from ninja import ModelSchema +from ninja import ModelSchema, Schema +from pydantic import condecimal from ml_model.models import ( ConfigurationParameter, @@ -32,3 +33,14 @@ class NeuronModelLink(ModelSchema): class Meta: model = NeuronModel fields = ('title', 'slug', 'alternative_titles') + + +class PredictPriceInputSchema(Schema): + model_slug: str + content: str + file_exists: bool + info: dict[str, Any] + + +class PredictPriceSchema(Schema): + price: condecimal(max_digits=10, decimal_places=2) | None \ No newline at end of file @@ -150,6 +150,7 @@ async def list_payment_plans(request): price=plan.price, tokens_per_plan=plan.tokens_per_plan, duration=plan.duration, + is_corporate=plan.is_corporate, points=plan.points, grouped_features=[{'name': cat, 'features': feats} for cat, feats in grouped.items()], individual=plan.individual, @@ -25,6 +25,7 @@ class PaymentPlanSchema(Schema): price: condecimal(max_digits=10, decimal_places=2) tokens_per_plan: condecimal(max_digits=10, decimal_places=2) duration: str + is_corporate: bool points: list[str] grouped_features: list[GroupedPlanFeatureSchema] = [] accessed_models: list[str] = [] @@ -10,6 +10,7 @@ class PaymentPlanSerializer(serializers.Serializer): price = serializers.DecimalField(max_digits=10, decimal_places=2) tokens_per_plan = serializers.DecimalField(max_digits=50, decimal_places=2) duration = serializers.CharField(read_only=True) + is_corporate = serializers.BooleanField() accessed_models = serializers.SerializerMethodField() individual = serializers.BooleanField()