Files
english/.opencode/skills/ai-multimodal/scripts/tests/test_minimax_generate.py
2026-04-12 01:06:31 +07:00

394 lines
14 KiB
Python

"""
Tests for minimax_generate.py - generation functions for image, video, speech, music.
"""
import json
import pytest
import sys
import time
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock, call
sys.path.insert(0, str(Path(__file__).parent.parent))
import minimax_generate as mg
class TestModelRegistries:
"""Test model set definitions."""
def test_image_models(self):
assert 'image-01' in mg.MINIMAX_IMAGE_MODELS
assert 'image-01-live' in mg.MINIMAX_IMAGE_MODELS
def test_video_models(self):
assert 'MiniMax-Hailuo-2.3' in mg.MINIMAX_VIDEO_MODELS
assert 'MiniMax-Hailuo-2.3-Fast' in mg.MINIMAX_VIDEO_MODELS
assert 'S2V-01' in mg.MINIMAX_VIDEO_MODELS
def test_speech_models(self):
assert 'speech-2.8-hd' in mg.MINIMAX_SPEECH_MODELS
assert 'speech-2.8-turbo' in mg.MINIMAX_SPEECH_MODELS
def test_music_models(self):
assert 'music-2.5' in mg.MINIMAX_MUSIC_MODELS
def test_all_models_is_union(self):
expected = (mg.MINIMAX_IMAGE_MODELS | mg.MINIMAX_VIDEO_MODELS |
mg.MINIMAX_SPEECH_MODELS | mg.MINIMAX_MUSIC_MODELS)
assert mg.ALL_MINIMAX_MODELS == expected
class TestIsMinimaxModel:
"""Test model detection."""
def test_known_image_model(self):
assert mg.is_minimax_model('image-01') is True
def test_known_video_model(self):
assert mg.is_minimax_model('MiniMax-Hailuo-2.3') is True
def test_known_speech_model(self):
assert mg.is_minimax_model('speech-2.8-hd') is True
def test_known_music_model(self):
assert mg.is_minimax_model('music-2.5') is True
def test_prefix_minimax(self):
assert mg.is_minimax_model('MiniMax-Future-Model') is True
def test_prefix_speech(self):
assert mg.is_minimax_model('speech-3.0-ultra') is True
def test_prefix_s2v(self):
assert mg.is_minimax_model('S2V-02') is True
def test_non_minimax_model(self):
assert mg.is_minimax_model('gemini-2.5-flash') is False
def test_non_minimax_imagen(self):
assert mg.is_minimax_model('imagen-4.0-generate-001') is False
class TestGenerateImage:
"""Test image generation."""
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"image_urls": ["https://cdn.minimax.io/img1.png"]}
}
with patch('requests.get') as mock_req_get:
mock_resp = Mock()
mock_resp.content = b'\x89PNG\r\n\x1a\n'
mock_resp.raise_for_status = Mock()
mock_req_get.return_value = mock_resp
result = mg.generate_image("key", "A cat", "image-01")
assert result["status"] == "success"
assert len(result["generated_images"]) == 1
assert result["model"] == "image-01"
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_no_images_returns_error(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"data": {"image_urls": []}}
result = mg.generate_image("key", "A cat", "image-01")
assert result["status"] == "error"
@patch('minimax_generate.api_post')
def test_payload_structure(self, mock_post):
mock_post.return_value = {"data": {"image_urls": []}}
mg.generate_image("key", "A dog", "image-01", "16:9", 3)
payload = mock_post.call_args[0][1]
assert payload["model"] == "image-01"
assert payload["prompt"] == "A dog"
assert payload["aspect_ratio"] == "16:9"
assert payload["n"] == 3
assert payload["response_format"] == "url"
assert payload["prompt_optimizer"] is True
@patch('minimax_generate.api_post')
def test_num_images_capped_at_9(self, mock_post):
mock_post.return_value = {"data": {"image_urls": []}}
mg.generate_image("key", "test", "image-01", num_images=15)
payload = mock_post.call_args[0][1]
assert payload["n"] == 9
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_output_copy(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"image_urls": ["https://cdn.minimax.io/img.png"]}
}
with patch('requests.get') as mock_req_get:
mock_resp = Mock()
mock_resp.content = b'image_bytes'
mock_resp.raise_for_status = Mock()
mock_req_get.return_value = mock_resp
output_path = str(tmp_path / "custom_output.png")
result = mg.generate_image("key", "test", output=output_path)
assert Path(output_path).exists()
class TestGenerateVideo:
"""Test video generation (async workflow)."""
@patch('minimax_generate.download_file')
@patch('minimax_generate.poll_async_task')
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success(self, mock_post, mock_dir, mock_poll, mock_dl, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"task_id": "vid-task-123"}
mock_poll.return_value = {"file_id": "file-456"}
# Create a fake video file so stat() works
mock_dl.side_effect = lambda fid, key, path, v: (
Path(path).write_bytes(b'fake_video') or path
)
result = mg.generate_video("key", "A dancer")
assert result["status"] == "success"
assert "generated_video" in result
assert result["model"] == "MiniMax-Hailuo-2.3"
mock_poll.assert_called_once()
@patch('minimax_generate.api_post')
def test_no_task_id_error(self, mock_post):
mock_post.return_value = {"error": "bad request"}
result = mg.generate_video("key", "test")
assert result["status"] == "error"
assert "No task_id" in result["error"]
@patch('minimax_generate.poll_async_task')
@patch('minimax_generate.api_post')
def test_no_file_id_error(self, mock_post, mock_poll):
mock_post.return_value = {"task_id": "t1"}
mock_poll.return_value = {"status": "Success"}
result = mg.generate_video("key", "test")
assert result["status"] == "error"
assert "No file_id" in result["error"]
@patch('minimax_generate.api_post')
def test_payload_with_first_frame(self, mock_post):
mock_post.return_value = {"task_id": None}
mg.generate_video("key", "test", first_frame="https://img.url/frame.png")
payload = mock_post.call_args[0][1]
assert payload["first_frame_image"] == "https://img.url/frame.png"
@patch('minimax_generate.api_post')
def test_payload_duration_resolution(self, mock_post):
mock_post.return_value = {"task_id": None}
mg.generate_video("key", "test", duration=10, resolution="720P")
payload = mock_post.call_args[0][1]
assert payload["duration"] == 10
assert payload["resolution"] == "720P"
class TestGenerateSpeech:
"""Test speech/TTS generation."""
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
# hex-encoded audio bytes
mock_post.return_value = {
"data": {"audio": "48656c6c6f"} # "Hello" in hex
}
result = mg.generate_speech("key", "Hello world")
assert result["status"] == "success"
assert "generated_audio" in result
assert result["model"] == "speech-2.8-hd"
# Verify file was written
audio_path = Path(result["generated_audio"])
assert audio_path.exists()
assert audio_path.read_bytes() == bytes.fromhex("48656c6c6f")
@patch('minimax_generate.api_post')
def test_no_audio_returns_error(self, mock_post):
mock_post.return_value = {"data": {}}
result = mg.generate_speech("key", "test")
assert result["status"] == "error"
@patch('minimax_generate.api_post')
def test_payload_structure(self, mock_post):
mock_post.return_value = {"data": {}}
mg.generate_speech("key", "Test text", "speech-2.8-turbo",
voice="English_Warm_Bestie", emotion="happy",
output_format="wav", rate=1.5)
payload = mock_post.call_args[0][1]
assert payload["model"] == "speech-2.8-turbo"
assert payload["text"] == "Test text"
assert payload["stream"] is False
assert payload["output_format"] == "hex"
assert payload["voice_setting"]["voice_id"] == "English_Warm_Bestie"
assert payload["voice_setting"]["speed"] == 1.5
assert payload["audio_setting"]["format"] == "wav"
assert payload["audio_setting"]["sample_rate"] == 32000
@patch('minimax_generate.api_post')
def test_text_truncated_at_10000(self, mock_post):
mock_post.return_value = {"data": {}}
long_text = "x" * 15000
mg.generate_speech("key", long_text)
payload = mock_post.call_args[0][1]
assert len(payload["text"]) == 10000
@patch('minimax_generate.api_post')
def test_uses_t2a_v2_endpoint(self, mock_post):
mock_post.return_value = {"data": {}}
mg.generate_speech("key", "test")
endpoint = mock_post.call_args[0][0]
assert endpoint == "t2a_v2"
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_wav_extension(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"data": {"audio": "aabb"}}
result = mg.generate_speech("key", "test", output_format="wav")
assert result["generated_audio"].endswith(".wav")
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_pcm_defaults_to_mp3_ext(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"data": {"audio": "aabb"}}
result = mg.generate_speech("key", "test", output_format="pcm")
assert result["generated_audio"].endswith(".mp3")
class TestGenerateMusic:
"""Test music generation."""
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success_with_url(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"audio": "https://cdn.minimax.io/music.mp3"},
"extra_info": {"music_duration": 120000}
}
with patch('requests.get') as mock_req_get:
mock_resp = Mock()
mock_resp.content = b'music_data'
mock_resp.raise_for_status = Mock()
mock_req_get.return_value = mock_resp
result = mg.generate_music("key", lyrics="La la la",
prompt="pop")
assert result["status"] == "success"
assert result["duration_ms"] == 120000
assert result["model"] == "music-2.5"
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success_with_hex(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"audio": "deadbeef"},
"extra_info": {"music_duration": 60000}
}
result = mg.generate_music("key", lyrics="test")
assert result["status"] == "success"
audio_path = Path(result["generated_audio"])
assert audio_path.read_bytes() == bytes.fromhex("deadbeef")
@patch('minimax_generate.api_post')
def test_no_audio_returns_error(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
result = mg.generate_music("key", lyrics="test")
assert result["status"] == "error"
@patch('minimax_generate.api_post')
def test_payload_structure(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="Verse 1\nHello",
prompt="upbeat pop", model="music-2.5",
output_format="wav")
payload = mock_post.call_args[0][1]
assert payload["model"] == "music-2.5"
assert payload["lyrics"] == "Verse 1\nHello"
assert payload["prompt"] == "upbeat pop"
assert payload["output_format"] == "url"
assert payload["audio_setting"]["format"] == "wav"
assert payload["audio_setting"]["sample_rate"] == 44100
@patch('minimax_generate.api_post')
def test_lyrics_truncated_at_3500(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="x" * 5000)
payload = mock_post.call_args[0][1]
assert len(payload["lyrics"]) == 3500
@patch('minimax_generate.api_post')
def test_prompt_truncated_at_2000(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", prompt="y" * 3000)
payload = mock_post.call_args[0][1]
assert len(payload["prompt"]) == 2000
@patch('minimax_generate.api_post')
def test_uses_300s_timeout(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="test")
# Check timeout kwarg passed to api_post
_, kwargs = mock_post.call_args
assert kwargs.get('timeout') == 300
@patch('minimax_generate.api_post')
def test_empty_lyrics_omitted(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="", prompt="jazz")
payload = mock_post.call_args[0][1]
assert "lyrics" not in payload
assert payload["prompt"] == "jazz"