From 169640cd72cd27d30fd71261474e2c7119b7ae6f Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:49:46 +0800 Subject: [PATCH] feat: support MiniMax image-to-image references --- .../minimax_model_provider/credential/tti.py | 16 ++- .../impl/minimax_model_provider/model/tti.py | 18 +++- tests/models_provider/test_minimax_tti.py | 101 ++++++++++++++++++ 3 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 tests/models_provider/test_minimax_tti.py diff --git a/apps/models_provider/impl/minimax_model_provider/credential/tti.py b/apps/models_provider/impl/minimax_model_provider/credential/tti.py index 71ea95a267c..2cb087b8893 100644 --- a/apps/models_provider/impl/minimax_model_provider/credential/tti.py +++ b/apps/models_provider/impl/minimax_model_provider/credential/tti.py @@ -5,15 +5,15 @@ from django.utils.translation import gettext_lazy as _, gettext from common import forms from common.exception.app_exception import AppApiException -from common.forms import BaseForm, PasswordInputField, SingleSelect, SliderField, TooltipLabel +from common.forms import BaseForm, PasswordInputField, SliderField, TooltipLabel from models_provider.base_model_provider import BaseModelCredential, ValidCode from common.utils.logger import maxkb_logger class MiniMaxModelParams(BaseForm): """ - Parameters class for the Qwen Text-to-Image model. - Defines fields such as image size, number of images, and style. + Parameters for MiniMax image generation. + Defines the output count and optional subject reference input. """ n = SliderField( @@ -26,6 +26,14 @@ class MiniMaxModelParams(BaseForm): precision=0 ) + subject_reference = forms.TextInputField( + TooltipLabel( + _('Subject reference image'), + _('Public image URL or base64 data URL used for image-to-image generation') + ), + required=False, + ) + class MiniMaxTextToImageModelCredential(BaseForm, BaseModelCredential): """ @@ -75,7 +83,7 @@ def is_valid( try: model = provider.get_model(model_type, model_name, model_credential, **model_params) - res = model.check_auth() + model.check_auth() except Exception as e: maxkb_logger.error(f'Exception: {e}', exc_info=True) if isinstance(e, AppApiException): diff --git a/apps/models_provider/impl/minimax_model_provider/model/tti.py b/apps/models_provider/impl/minimax_model_provider/model/tti.py index 5d2c3e0b4b7..15a77e3bc6a 100644 --- a/apps/models_provider/impl/minimax_model_provider/model/tti.py +++ b/apps/models_provider/impl/minimax_model_provider/model/tti.py @@ -1,10 +1,7 @@ # coding=utf-8 -from http import HTTPStatus from typing import Dict import requests -from dashscope import ImageSynthesis, MultiModalConversation -from dashscope.aigc.image_generation import ImageGeneration from common.utils.logger import maxkb_logger from models_provider.base_model_provider import MaxKBBaseModel @@ -47,14 +44,25 @@ def new_instance(model_type, model_name, model_credential: Dict[str, object], ** def check_auth(self): return True - def generate_image(self, prompt: str, negative_prompt: str = None): + def generate_image(self, prompt: str, negative_prompt: str = None, subject_reference=None): headers = {"Authorization": f"Bearer {self.api_key}"} + params = self.params or {} + subject_reference = ( + subject_reference if subject_reference is not None else params.get('subject_reference') + ) + payload = { "model": self.model_name, "prompt": prompt, - **self.params, + **{key: value for key, value in params.items() if key != 'subject_reference'}, } + if subject_reference: + if isinstance(subject_reference, str): + subject_reference = [{"type": "character", "image_file": subject_reference}] + elif not isinstance(subject_reference, list): + raise ValueError('subject_reference must be an image URL, data URL, or list of reference objects') + payload['subject_reference'] = subject_reference try: response = requests.post(f'{self.api_base}/image_generation', headers=headers, json=payload) response.raise_for_status() diff --git a/tests/models_provider/test_minimax_tti.py b/tests/models_provider/test_minimax_tti.py new file mode 100644 index 00000000000..e2e60559031 --- /dev/null +++ b/tests/models_provider/test_minimax_tti.py @@ -0,0 +1,101 @@ +import importlib.util +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + + +class DummyMaxKBBaseModel: + def __init__(self, **kwargs): + pass + + +class DummyBaseTextToImage: + pass + + +def load_tti_module(): + requests_module = types.ModuleType('requests') + requests_module.post = Mock() + + common_module = types.ModuleType('common') + common_utils_module = types.ModuleType('common.utils') + logger_module = types.ModuleType('common.utils.logger') + logger_module.maxkb_logger = Mock() + + models_provider_module = types.ModuleType('models_provider') + base_model_provider_module = types.ModuleType('models_provider.base_model_provider') + base_model_provider_module.MaxKBBaseModel = DummyMaxKBBaseModel + impl_module = types.ModuleType('models_provider.impl') + base_tti_module = types.ModuleType('models_provider.impl.base_tti') + base_tti_module.BaseTextToImage = DummyBaseTextToImage + + modules = { + 'requests': requests_module, + 'common': common_module, + 'common.utils': common_utils_module, + 'common.utils.logger': logger_module, + 'models_provider': models_provider_module, + 'models_provider.base_model_provider': base_model_provider_module, + 'models_provider.impl': impl_module, + 'models_provider.impl.base_tti': base_tti_module, + } + + module_path = ( + Path(__file__).resolve().parents[2] + / 'apps/models_provider/impl/minimax_model_provider/model/tti.py' + ) + spec = importlib.util.spec_from_file_location('minimax_tti_under_test', module_path) + module = importlib.util.module_from_spec(spec) + with patch.dict(sys.modules, modules): + spec.loader.exec_module(module) + return module, requests_module + + +class MiniMaxTextToImageModelTest(unittest.TestCase): + def setUp(self): + module, self.requests = load_tti_module() + response = Mock() + response.json.return_value = {'data': {'image_urls': ['https://example.com/generated.png']}} + self.requests.post.return_value = response + self.model = module.MiniMaxTextToImageModel( + api_key='test-key', + api_base='https://api.example.com/v1', + model_name='image-01', + params={'n': 1}, + ) + + def test_adds_subject_reference_from_model_params(self): + reference_url = 'https://example.com/reference.png' + self.model.params['subject_reference'] = reference_url + + result = self.model.generate_image('Create a portrait') + + payload = self.requests.post.call_args.kwargs['json'] + self.assertEqual(result, ['https://example.com/generated.png']) + self.assertEqual( + payload['subject_reference'], + [{'type': 'character', 'image_file': reference_url}], + ) + self.assertEqual(payload['n'], 1) + + def test_keeps_text_to_image_payload_without_reference(self): + self.model.generate_image('Create a landscape') + + payload = self.requests.post.call_args.kwargs['json'] + self.assertNotIn('subject_reference', payload) + + def test_accepts_structured_subject_reference_argument(self): + subject_reference = [ + {'type': 'character', 'image_file': 'data:image/png;base64,AAAA'}, + ] + + self.model.generate_image('Create a portrait', subject_reference=subject_reference) + + payload = self.requests.post.call_args.kwargs['json'] + self.assertEqual(payload['subject_reference'], subject_reference) + + +if __name__ == '__main__': + unittest.main()