@@ -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) + @@ -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), @@ -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') @@ -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 @@ -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 @@ -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 @@ -21,6 +22,10 @@ from tools.media.models import Preset class Minimaxmusic(SimpleService): TOKENS_COST = Decimal('10.5') + @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 @@ -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, @@ -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] @@ -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 Wan(SimpleService): '720p': Decimal('50') } + @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') @@ -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