@@ -1236,3 +1236,12 @@ msgstr "Плательщик не существует" msgid "The request must not be empty" msgstr "Запрос не должен быть пустым" + +msgid "Your request was blocked by our moderation system" +msgstr "Ваш запрос был заблокирован нашей системой модерации" + +msgid "Image size %dx%d is not supported. Required size: 720x1280" +msgstr "Размер изображения %sx%s не поддерживается. Требуемый размер: 720x1280" + +msgid "Image size 1280x720 is not supported. Please rotate image to 720x1280" +msgstr "Размер изображения 1280x720 не поддерживается. Пожалуйста, переверните до 720x1280" \ No newline at end of file @@ -12,6 +12,7 @@ from ml_model.services.fluxproultra import Fluxproultra from ml_model.services.gemini import Gemini from ml_model.services.geminiimage import Geminiimage from ml_model.services.gptimage import Gptimage +from ml_model.services.sora import Sora from ml_model.services.granite import Granite from ml_model.services.grok import Grok from ml_model.services.iconic import Iconic @@ -0,0 +1,109 @@ +import time +import filetype + +from datetime import timedelta +from decimal import Decimal +from io import BytesIO +from PIL import Image + +import httpx +from django.conf import settings +from django.core.files import File + +from messages.models import Message +from ml_model.exceptions import ModelTimeoutError, RequestBlocked, UnsupportedSize +from ml_model.services.base import SimpleService +from poller.models import Proxy + + + +class Sora(SimpleService): + + TOKENS_COST = { + 'sora-2': Decimal('30'), + 'sora-2-pro': Decimal('90'), + } + + def calculate_price(self, seconds: int, version) -> Decimal: + price = Decimal(seconds) * self.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, + content_object=self.store, + elapsed_time=t, + file=File(BytesIO(video), '.mp4'), + ) + if save: + return Message.objects.bulk_create([msg]) + return [msg] + + def make(self, input_message: "Message", save: bool = True) -> list["Message"]: + if input_message.content: + for proxy in Proxy.objects.all(): + version = input_message.info.pop('version', 'sora-2') + seconds = int(input_message.info.pop('seconds', '4')) + callback_data = { + 'prompt': input_message.content, + 'model': version, + 'seconds': str(seconds), + **input_message.info + } + files = None + if input_message.file: + kind = filetype.guess(input_message.file.read(20)) + mime_type = kind.mime if kind else 'application/octet-stream' + input_message.file.seek(0) + img = Image.open(input_message.file) + width, height = img.size + required_size = (720, 1280) + current_size = (width, height) + if current_size not in required_size: + raise UnsupportedSize(current_size, required_size) + buf = BytesIO() + img.save(buf, format="PNG") + buf.seek(0) + files = { + "input_reference": ( + input_message.file.name, + buf, + mime_type + ) + } + with httpx.Client( + base_url='https://api.openai.com/v1/', + proxy=f'{proxy.protocol}://{proxy.address}' if proxy else None, + headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, + timeout=600, + ) as client: + start_time = time.time() + if files: + resp = client.post("videos", data=callback_data, files=files) + else: + resp = client.post("videos", json=callback_data) + if resp.status_code not in (200, 201): + continue + video_info = resp.json() + video_id = video_info.get("id") + while True: + status_resp = client.get(f"videos/{video_id}") + status = status_resp.json().get("status") + video_data = status_resp.json() + if status == "completed": + break + if status == "failed": + error_message = video_data.get("error", {}).get("message", "Unknown error") + if error_message == "Your request was blocked by our moderation system.": + raise RequestBlocked + raise Exception("Video generation failed") + time.sleep(1/3) + video = client.get(f"videos/{video_id}/content").content + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, seconds, version + ) + msgs = self.save_results(input_message.content, process_time, video, save) + return msgs + raise ModelTimeoutError + raise ModelTimeoutError @@ -16,12 +16,29 @@ class DeploymentDisabled(Exception): def __str__(self): return _('The model is currently disabled. Please try again later.') +class RequestBlocked(Exception): + def __str__(self): + return _('Your request was blocked by our moderation system') + +class UnsupportedSize(Exception): + def __init__(self, current_size: tuple[int, int], required_size: tuple[int, int]): + self.current_size = current_size + self.required_size = required_size + + def __str__(self): + if self.current_size == self.required_size[::-1]: + return _('Image size %dx%d is not supported. Please rotate image to %dx%d') % ( + *self.current_size, *self.required_size + ) + else: + return _('Image size %dx%d is not supported. Required size: %dx%d') % ( + *self.current_size, *self.required_size + ) class ModelTimeoutError(Exception): def __str__(self): return _('The model is not responding') - class FileExtensionNotSupported(Exception): def __init__(self, extensions: list[str]) -> None: self.extensions = extensions