From c13c80ff208ac9f6ce61ebc3bca9dc2c40f9c838 Mon Sep 17 00:00:00 2001 From: djj <421774554@qq.com> Date: Wed, 11 Nov 2026 10:27:58 +0800 Subject: [PATCH 1/2] [feat] providers: add MiniMax speech with system voices ## summary - Add mainland MiniMax synchronous TTS and a read-only system voice library through the shared generation and voice services. - Preserve provider voice IDs, validate accepted inputs, classify HTTP-200 failures, and recover public MP3 downloads without reposting. - Add bilingual presets, errors, docs, browser evidence and regression tests for UI/Agent use, persisted media and recovery. ## validation - MiniMax speech: 58 tests passed; related generation, model, voice, media download security and contract suites passed. - npm test: 768 passed. Build, import boundaries, generated contracts, i18n, browser locale and manifest checks passed. - Bilingual voice-library browser workflow passed with mocked HTTP and local WAV playback. Python cases probe real MP3 fixture bytes. - No live MiniMax calls or perceptual voice-quality checks were made. Refs: #240 --- apps/api/src/opendirector_api/localization.py | 3 + docs/development/validation.md | 3 + .../images/minimax-speech-voices-en-US.png | Bin 0 -> 63244 bytes .../images/minimax-speech-voices-zh-CN.png | Bin 0 -> 91153 bytes docs/guides/user-guide.md | 8 +- docs/guides/user-guide.zh-CN.md | 8 +- .../application/creation_generation.py | 4 +- .../opendirector/application/voice_library.py | 21 +- .../opendirector/domain/creation/commands.py | 1 + .../domain/creation/generation.py | 6 +- .../opendirector/domain/creation/models.py | 2 +- .../src/opendirector/domain/input_rules.py | 5 + .../src/opendirector/domain/minimax_speech.py | 29 ++ .../core/src/opendirector/domain/voices.py | 12 +- .../integrations/generation_errors.py | 7 +- .../integrations/model_adapters.py | 52 ++- .../opendirector/integrations/providers.py | 11 +- .../integrations/reference_inputs.py | 2 + src/canvas/generationFailureText.ts | 1 + src/canvas/modelBrands.ts | 1 + src/canvas/providerPresets.ts | 12 + src/contracts/creation.schema.json | 12 +- src/contracts/generated.ts | 8 +- src/i18n/messages/en-US/errors.ts | 2 +- src/i18n/messages/zh-CN/errors.ts | 2 +- tests/browser/voice_library.mjs | 74 +++- tests/browser/voice_library_server.py | 8 + tests/generationFailureText.test.ts | 16 +- tests/modelBrands.test.ts | 3 + tests/providerPresets.test.ts | 18 + tests/test_minimax_speech.py | 374 ++++++++++++++++++ 31 files changed, 679 insertions(+), 26 deletions(-) create mode 100644 docs/guides/images/minimax-speech-voices-en-US.png create mode 100644 docs/guides/images/minimax-speech-voices-zh-CN.png create mode 100644 packages/core/src/opendirector/domain/minimax_speech.py create mode 100644 tests/test_minimax_speech.py diff --git a/apps/api/src/opendirector_api/localization.py b/apps/api/src/opendirector_api/localization.py index c5a4cbf6..a31dcaf0 100644 --- a/apps/api/src/opendirector_api/localization.py +++ b/apps/api/src/opendirector_api/localization.py @@ -50,6 +50,9 @@ "minimax_image_reference": "MiniMax H3 references must be PNG, JPEG, or WebP, with edges of 128–8192 pixels and a long-to-short edge ratio of at most 4:1.", "minimax_image_count": "MiniMax H3 image editing accepts up to 16 reference images.", "minimax_image_bytes": "MiniMax H3 image reference requests cannot exceed 64 MiB.", + "minimax_speech_text": "Enter the text to read aloud, up to 9,999 characters.", + "minimax_speech_voice": "Choose a voice available to your MiniMax account from the voice library.", + "minimax_speech_reference": "MiniMax speech synthesis does not accept references. Enter the text to read aloud and choose a voice.", "moss_image_size": "MOSS image dimensions must be 256–2048, multiples of 16, and no more than 2,097,152 pixels in total.", "moss_image_reference": "MOSS image generation does not accept image references or editing inputs.", "moss_layout_reference": "MOSS layout JSON does not accept additional text references. Put the requirements in the JSON or remove the text references.", diff --git a/docs/development/validation.md b/docs/development/validation.md index 71e2d2b5..cecbf452 100644 --- a/docs/development/validation.md +++ b/docs/development/validation.md @@ -269,6 +269,7 @@ The voice browser workflow uses a separate isolated fixture. It runs the real sh ```sh .venv/bin/python -m pytest tests/test_voice_library.py -q +.venv/bin/python -m pytest tests/test_minimax_speech.py -q node --test tests/voiceLibrary.test.ts tests/creationParameters.test.ts # Terminal 1 .venv/bin/python tests/browser/voice_library_server.py --port 8030 @@ -283,3 +284,5 @@ For a slow-response regression, set `VOICE_REOPEN_DELAY_MS=17000` for the browse 慢响应回归:在浏览器命令的环境变量中设置 `VOICE_REOPEN_DELAY_MS=17000`,让重开后的资源请求超过普通交互的 15 秒等待。冷启动导航、保存状态和重开媒体分别有明确的 45 秒上限;HTTP 错误仍立即报告状态并失败,不重试生成。 The bilingual browser workflow covers opening/outside click/Escape, keyboard tabs, four-column and narrow layouts, real previews and unavailable previews, metadata filters, scoped favorites, loading/error/retry, invalid and valid local audio, creation, selection, Drama3 transcript and Nano reference requirements, supported deletion and failure feedback, model/service switching, project reopening and persisted job input. `moss_speech.mjs` is retained as an alias for this integrated workflow. The pagination regression includes a 15,001-voice catalog. With a configured MOSS account, open the library, go to the next page, and return to the first. Verify the page counts and that returning to the first page restores its entries using read-only requests. Python tests verify provider fields, pagination, credentials, account boundaries, actual audio/container/duration validation, and equivalent Agent operations. Actual provider synthesis still requires a configured service, account permission and a recording authorized for use. + +The same browser workflow also covers MiniMax's mainland speech preset in both languages, saved credentials remaining private, read-only system voices, IDs containing spaces, selection retention between HD and Turbo, the 9,999-character precheck, and generation/reopen/playback. Its generated audio is a local WAV fixture, not MiniMax output. `test_minimax_speech.py` uses mocked HTTP with real MP3 bytes to check `/t2a_v2` and `/get_voice`, HTTP-200 error envelopes, invalid or incomplete responses, public result downloads without credentials, UI/Agent acceptance and receipts, actual media probing, and recovery after model removal without another synthesis call. Live MiniMax synthesis and perceptual voice quality require a separately authorized real-account run. diff --git a/docs/guides/images/minimax-speech-voices-en-US.png b/docs/guides/images/minimax-speech-voices-en-US.png new file mode 100644 index 0000000000000000000000000000000000000000..8ac142cabaa042bc92e3da640468a92ec8fe1f0c GIT binary patch literal 63244 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 X9nvyy6t$rRYE(RS?Q_5D7M}SpX-zV2 literal 0 HcmV?d00001 diff --git a/docs/guides/images/minimax-speech-voices-zh-CN.png b/docs/guides/images/minimax-speech-voices-zh-CN.png new file mode 100644 index 0000000000000000000000000000000000000000..b6b08edf01a2cf8a65552924ae90f781041ac92e GIT binary patch literal 91153 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\KYN|l}XBquE$ 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 10000 or not all(isinstance(item, dict) for item in items): + raise voice_error('voice_provider_protocol', 502) + return [self._record({'id': item.get('voice_id'), 'name': item.get('voice_name')}, capabilities, + source='builtin', has_audio=False, config=config) for item in items], None + @staticmethod def _valid_cursor(cursor): return isinstance(cursor, str) and 0 < len(cursor) <= 2048 and cursor == cursor.strip() and cursor.isprintable() @@ -169,6 +184,8 @@ def _pages(self, config, capabilities, limit, after=None): self._assert_current(config, capabilities) if capabilities['provider'] == 'moss': records, cursor = self._moss_page(config, capabilities, limit, after) + elif capabilities['provider'] == 'minimax': + records, cursor = self._minimax_page(config, capabilities) else: records, cursor = self._vllm_page(config, capabilities) for record, _ in records: diff --git a/packages/core/src/opendirector/domain/creation/commands.py b/packages/core/src/opendirector/domain/creation/commands.py index d712a054..96d7f1bf 100644 --- a/packages/core/src/opendirector/domain/creation/commands.py +++ b/packages/core/src/opendirector/domain/creation/commands.py @@ -402,6 +402,7 @@ def command_fingerprint(command: Command) -> str: 'moss_speech_text', 'moss_speech_voice', 'moss_speech_reference', 'moss_prompt_limit', 'moss_image_size', 'moss_image_reference', 'moss_layout_reference', 'minimax_image_reference', 'minimax_image_count', 'minimax_image_bytes', + 'minimax_speech_text', 'minimax_speech_voice', 'minimax_speech_reference', 'voice_library_unsupported', 'voice_connection_changed', 'voice_reference_required', 'voice_transcript_required', 'voice_transcript_too_long', 'voice_description_too_long', 'voice_not_found', 'voice_delete_unsupported', diff --git a/packages/core/src/opendirector/domain/creation/generation.py b/packages/core/src/opendirector/domain/creation/generation.py index 4040b3b0..8f745d1a 100644 --- a/packages/core/src/opendirector/domain/creation/generation.py +++ b/packages/core/src/opendirector/domain/creation/generation.py @@ -368,10 +368,12 @@ def validate_generation(spec: GenerationSpec, model: ModelConfigPublic, referenc rules = model.inputRules if rules.referencesAllowed is False and references: raise ValueError('当前模型不支持参考素材,请移除参考后重试。') - if rules.profile == 'moss-speech': + if rules.profile in {'moss-speech', 'minimax-speech'}: + from opendirector.domain.minimax_speech import validate_minimax_speech_input from opendirector.domain.moss_speech import validate_moss_speech_input text = spec.prompt if spec.promptContent is None else ''.join(part.text for part in spec.promptContent if part.type == 'text') - validate_moss_speech_input(text, spec.parameters.voice) + validate = validate_moss_speech_input if rules.profile == 'moss-speech' else validate_minimax_speech_input + validate(text, spec.parameters.voice) elif spec.kind == 'audio' and rules.voiceRequired and not spec.parameters.voice: raise ValueError('请填写服务音色 ID。') if len(references) > MAX_REFERENCES: diff --git a/packages/core/src/opendirector/domain/creation/models.py b/packages/core/src/opendirector/domain/creation/models.py index 937a2d0f..b652064d 100644 --- a/packages/core/src/opendirector/domain/creation/models.py +++ b/packages/core/src/opendirector/domain/creation/models.py @@ -29,7 +29,7 @@ class ImageOutput(Contract): class VoiceLibraryCapabilities(Contract): - provider: Literal['vllm_omni', 'moss'] = Field(description='Voice registry protocol.') + provider: Literal['vllm_omni', 'moss', 'minimax'] = Field(description='Voice registry protocol.') connectionId: str = Field(min_length=1, max_length=200, description='Opaque service, endpoint and account identity.') requiresTranscript: bool = Field(description='Registered voice must have a reference transcript.') requiresReferenceAudio: bool = Field(description='Registered voice must have actual reference audio.') diff --git a/packages/core/src/opendirector/domain/input_rules.py b/packages/core/src/opendirector/domain/input_rules.py index 80532a38..2a75e0d3 100644 --- a/packages/core/src/opendirector/domain/input_rules.py +++ b/packages/core/src/opendirector/domain/input_rules.py @@ -5,6 +5,7 @@ from opendirector.domain.video_capabilities import capability_fields, seedance_20_variant from .moss_image import MOSS_IMAGE_PROMPT_CHARACTERS, is_moss_image from .moss_speech import is_moss_speech +from .minimax_speech import MINIMAX_SPEECH_TEXT_CHARACTERS, is_minimax_speech from .kling_models import KLING_MODELS, KLING_RATIOS, kling_capabilities VideoMode = Literal['first_frame', 'reference', 'continuation'] @@ -32,6 +33,10 @@ def _adapter_rules(config): rules.update(profile='moss-speech', allowedKinds=[], maxImages=0, voiceRequired=True, referencesAllowed=False) return rules + if is_minimax_speech(config): + rules.update(profile='minimax-speech', allowedKinds=[], maxImages=0, voiceRequired=True, + referencesAllowed=False, maxPromptCharacters=MINIMAX_SPEECH_TEXT_CHARACTERS) + return rules if kind == 'vision' or config.get('protocol') == 'vllm_omni_segmentation': # Shared image segmentation / video tracking service, never a generator. rules.update(profile='vllm-omni-segmentation', allowedKinds=[], maxImages=0, videoModes=[]) diff --git a/packages/core/src/opendirector/domain/minimax_speech.py b/packages/core/src/opendirector/domain/minimax_speech.py new file mode 100644 index 00000000..8ad99d74 --- /dev/null +++ b/packages/core/src/opendirector/domain/minimax_speech.py @@ -0,0 +1,29 @@ +"""Model identity and input rules for MiniMax synchronous speech synthesis (T2A v2).""" +from urllib.parse import urlsplit + +from .projects import ProjectError + +# https://platform.minimaxi.com/docs/api-reference/speech-t2a-http +MINIMAX_SPEECH_MODELS = frozenset({ + 'speech-2.8-hd', 'speech-2.8-turbo', 'speech-2.6-hd', 'speech-2.6-turbo', + 'speech-02-hd', 'speech-02-turbo', 'speech-01-hd', 'speech-01-turbo', +}) +# The synchronous API requires text shorter than 10,000 characters. +MINIMAX_SPEECH_TEXT_CHARACTERS = 9999 + + +def is_minimax_speech(config): + url = urlsplit(config.get('url', '')) + return (config.get('kind') == 'audio' and config.get('protocol') == 'openai' + and config.get('connectionType', 'api') == 'api' + and url.scheme == 'https' and url.hostname == 'api.minimax.cn' and url.port in {None, 443} + and url.path.rstrip('/') == '/v1' and config.get('modelId') in MINIMAX_SPEECH_MODELS) + + +def validate_minimax_speech_input(text, voice): + if not text.strip() or len(text) > MINIMAX_SPEECH_TEXT_CHARACTERS: + raise ProjectError('请填写需要朗读的文本,且不超过 9,999 个字符。', 422, code='minimax_speech_text') + # System voice IDs contain spaces and parentheses, and one ends with a + # space ("Santa_Claus "), so the ID is sent exactly as listed. + if not isinstance(voice, str) or not voice.strip() or len(voice) > 256 or not voice.isprintable(): + raise ProjectError('请从音色库选择 MiniMax 账户可用的音色。', 422, code='minimax_speech_voice') diff --git a/packages/core/src/opendirector/domain/voices.py b/packages/core/src/opendirector/domain/voices.py index 791bcd53..7a00bf5d 100644 --- a/packages/core/src/opendirector/domain/voices.py +++ b/packages/core/src/opendirector/domain/voices.py @@ -4,6 +4,7 @@ import re from urllib.parse import urlsplit, urlunsplit +from .minimax_speech import is_minimax_speech from .moss_speech import is_moss_speech from .projects import ProjectError @@ -43,16 +44,16 @@ def voice_library_capabilities(config): if config.get('kind') != 'audio' or config.get('protocol') != 'openai': return None name = config.get('modelId', '').rsplit('/', 1)[-1].lower().replace('_', '-') - moss = is_moss_speech(config) + moss, minimax = is_moss_speech(config), is_minimax_speech(config) cosyvoice = name.startswith(('cosyvoice3', 'fun-cosyvoice3')) fish_speech = name.startswith('fish-speech') or name in {'s2-pro', 'drama-3', 'drama-3-int4'} # Explicit served-model families; an arbitrary compatible endpoint is not # evidence that the server implements the vLLM voice registry. known = cosyvoice or fish_speech or name.startswith(('moss-tts', 'qwen3-tts', 'fishaudio', 'index-tts', 'indextts', 'omnivoice', 'higgs-audio', 'glm-tts', 'ming-', 'drama3')) - if not moss and not (config.get('connectionType') == 'self_hosted' and known): + if not moss and not minimax and not (config.get('connectionType') == 'self_hosted' and known): return None - provider = 'moss' if moss else 'vllm_omni' + provider = 'moss' if moss else 'minimax' if minimax else 'vllm_omni' parsed = urlsplit(config['url']) host = parsed.hostname.lower() if ':' in host: host = '[' + host + ']' @@ -62,6 +63,11 @@ def voice_library_capabilities(config): account = hashlib.sha256((config.get('apiKey', '') if config.get('authMode') != 'none' else '').encode()).hexdigest() identity = [provider, config.get('providerId') or config.get('id'), endpoint, config.get('authMode', 'none'), account] connection = hashlib.sha256(json.dumps(identity, separators=(',', ':')).encode()).hexdigest() + if minimax: + # Read-only system voices. MiniMax cloning and voice design are not adapted. + return dict(provider=provider, connectionId=connection, requiresTranscript=False, + requiresReferenceAudio=False, supportsUploadedVoices=False, referenceEmbeddingDimension=None, + canDelete=False, maxBytes=10 * 1024 * 1024, formats=[], minSeconds=None, maxSeconds=None) # Upstream vllm_omni/entrypoints/openai/tts_adapters/moss_tts.py: # validate() requires reference audio for standard/realtime/local/Nano. # Local-Transformer ref_text is optional continuation conditioning; Drama3 diff --git a/packages/core/src/opendirector/integrations/generation_errors.py b/packages/core/src/opendirector/integrations/generation_errors.py index a5a73cb1..835c6bb6 100644 --- a/packages/core/src/opendirector/integrations/generation_errors.py +++ b/packages/core/src/opendirector/integrations/generation_errors.py @@ -92,6 +92,11 @@ def provider_failure(value, *, stage, headers=None, http_status=None, secrets=() if code in {'1300', '1301'}: message, suggestion = ('供应商判定提示词或参考图片违反内容政策,已拒绝生成。', '请调整提示词或参考素材后再生成;当前输入和已有结果保留。') + if provider == 'minimax': + # MiniMax open-platform base_resp.status_code values, delivered with HTTP 200. + category = {'1001': 'temporary', '1002': 'rate_limit', '1039': 'rate_limit', '1004': 'authentication', + '2049': 'authentication', '1008': 'quota', '1042': 'input', '2013': 'input'}.get(code, category) + message, suggestion = _REASONS[category] diagnostic = error.get('message', '') diagnostic = diagnostic[:8192].lower() if isinstance(diagnostic, str) else '' if http_status in {400, 422} and category == 'input': @@ -101,7 +106,7 @@ def provider_failure(value, *, stage, headers=None, http_status=None, secrets=() elif re.search(r'(?:not support|unsupported|not supported)', diagnostic) and re.search(r'(?:reference_(?:image|video|audio)|image_url|video_url|audio_url|input_audio)', diagnostic): category, message = 'reference', '供应商拒绝了参考类型或输入组合。' suggestion = '请核对参考类型和生成模式,或选择其他已适配模型;当前输入和已有结果保留。' - if name == 'voice_not_found': + if name == 'voice_not_found' or provider == 'minimax' and code == '2054': category, message = 'input', '所选音色不存在或不可用。' suggestion = '请检查音色 ID 是否属于当前账户可用的音色。' elif name == 'inputimagesensitivecontentdetected.privacyinformation': diff --git a/packages/core/src/opendirector/integrations/model_adapters.py b/packages/core/src/opendirector/integrations/model_adapters.py index a78204ab..37aa9d89 100644 --- a/packages/core/src/opendirector/integrations/model_adapters.py +++ b/packages/core/src/opendirector/integrations/model_adapters.py @@ -22,6 +22,7 @@ VideoRequest) from opendirector.domain.input_rules import input_rules from opendirector.domain.minimax_image import validate_minimax_image_reference +from opendirector.domain.minimax_speech import validate_minimax_speech_input from opendirector.domain.moss_image import validate_moss_image_prompt, validate_moss_image_size from opendirector.domain.moss_speech import validate_moss_speech_input from opendirector.domain.projects import ProjectError @@ -207,12 +208,57 @@ async def submit(): async def download(self, config, url): data = await _download_moss_result(self, config, url) - mpeg_frame = len(data) >= 4 and data[0] == 0xff and data[1] & 0xe0 == 0xe0 and data[1] & 0x06 - if not data.startswith(b'ID3') and not mpeg_frame: + if not _is_mp3(data): raise protocol_failure('download') return data +def _is_mp3(data): + mpeg_frame = len(data) >= 4 and data[0] == 0xff and data[1] & 0xe0 == 0xe0 and data[1] & 0x06 + return data.startswith(b'ID3') or bool(mpeg_frame) + + +class MinimaxSpeech(ModelAdapter): + """MiniMax synchronous T2A v2, non-streaming, answering with a result URL.""" + capabilities = frozenset({'audio.speech'}) + + async def speech(self, config, request: SpeechRequest, *, result_ready=None) -> MediaResult: + validate_minimax_speech_input(request.text, request.voice) + payload = {'model': config['modelId'], 'text': request.text, 'stream': False, + 'voice_setting': {'voice_id': request.voice}, 'audio_setting': {'format': 'mp3'}, + 'output_format': 'url'} + + async def submit(): + secrets = (config.get('apiKey', ''),) + async with _client(self, config) as client: + async with client.stream('POST', endpoint(config['url'], '/t2a_v2'), + headers=auth_headers(config), json=payload) as response: + value = json.loads(await bounded(response, stage='submit', secrets=secrets, limit=1024 * 1024)) + # Errors arrive as HTTP 200 with a nonzero base_resp.status_code. + status = value['base_resp']['status_code'] + if type(status) is not int: + raise protocol_failure('submit') + if status != 0: + raise provider_failure({'code': status, 'request_id': value.get('trace_id')}, stage='submit', + http_status=200, secrets=secrets, provider='minimax') + if type(value['data']['status']) is not int or value['data']['status'] != 2: + raise protocol_failure('submit') + return _result_url(value['data']['audio'], 'submit') + + url = await _call('submit', submit) + if result_ready: + await result_ready('mp3', url) + return await self.download(config, url) + + async def download(self, config, url): + # The signed result URL is public storage: it never receives the API key. + _, data = await VolcengineGeneration(None, transport=self.transport).resume_result( + config, 'mp3', url, public_only=True) + if not _is_mp3(data): + raise protocol_failure('download') + return MediaResult(data, 'mp3') + + class MinimaxH3Images(ModelAdapter): capabilities = frozenset({'image.generate', 'image.edit'}) max_images = 16 @@ -739,6 +785,8 @@ def adapter_for(config, transport=None, poll_interval=3) -> ModelAdapter | None: return MossImages(transport, poll_interval) if profile == 'moss-speech': return MossSpeech(transport, poll_interval) + if profile == 'minimax-speech': + return MinimaxSpeech(transport, poll_interval) if kind == 'image' and profile == 'vllm-omni-image': return OpenAIImages(transport, poll_interval, free_size=True, strength=True) if kind == 'image' and profile == 'vllm-omni-qwen-image': diff --git a/packages/core/src/opendirector/integrations/providers.py b/packages/core/src/opendirector/integrations/providers.py index 3c96dd02..d1afe3f5 100644 --- a/packages/core/src/opendirector/integrations/providers.py +++ b/packages/core/src/opendirector/integrations/providers.py @@ -18,7 +18,7 @@ from opendirector.integrations.kling_video import KlingVideoGeneration from opendirector.integrations.vllm_omni import VllmOmniGeneration from opendirector.integrations.vllm_omni_image import VllmOmniImageGeneration -from opendirector.integrations.model_adapters import MinimaxH3Images, MossImages, MossSpeech, OpenAISpeech, VllmOmniMusic, VllmOmniScore +from opendirector.integrations.model_adapters import MinimaxH3Images, MinimaxSpeech, MossImages, MossSpeech, OpenAISpeech, VllmOmniMusic, VllmOmniScore from opendirector.domain.capabilities import ImageRequest, MusicRequest, ScoreRequest, SpeechRequest from opendirector.integrations.model_adapters import OpenAISpeech, VllmOmniMusic, VllmOmniScore, image_parts from opendirector.domain.capabilities import MusicRequest, ScoreRequest, SpeechRequest @@ -37,6 +37,7 @@ def __init__(self, store, transport=None, poll_interval=3, retry_delays=(0.5, 1. self.minimax_image = MinimaxH3Images(transport, poll_interval) self.moss_image = MossImages(transport, poll_interval) self.moss_speech = MossSpeech(transport, poll_interval) + self.minimax_speech = MinimaxSpeech(transport, poll_interval) def _profile_adapter(self, config): if config and config.get('protocol') == 'kling_video': @@ -93,6 +94,9 @@ async def resume_result(self, config, suffix, url): return suffix, await self.moss_image.download(config, url) if config and input_rules(config)['profile'] == 'moss-speech': return suffix, await self.moss_speech.download(config, url) + if config and input_rules(config)['profile'] == 'minimax-speech': + result = await self.minimax_speech.download(config, url) + return result.suffix, result.data if not config or config.get('protocol') in {None, 'volcengine'}: return await super().resume_result(config, suffix, url) # Only send a service token back to that exact configured origin. @@ -131,6 +135,11 @@ async def generate(self, config, task, submitted, *, result_ready=None): result = await self.moss_speech.speech(config, SpeechRequest(prepared['prompt'], task['parameters'].get('voice')), result_ready=result_ready) return result.suffix, result.data + if input_rules(config)['profile'] == 'minimax-speech': + prepared = await asyncio.to_thread(generation_references, self.store, task, config) + result = await self.minimax_speech.speech(config, SpeechRequest(prepared['prompt'], task['parameters'].get('voice')), + result_ready=result_ready) + return result.suffix, result.data if config.get('protocol') in {None, 'volcengine'} or task['kind'] == 'text': return await super().generate(config, task, submitted, result_ready=result_ready) kind, stage = task['kind'], 'submit' diff --git a/packages/core/src/opendirector/integrations/reference_inputs.py b/packages/core/src/opendirector/integrations/reference_inputs.py index 9914a035..261fa1a8 100644 --- a/packages/core/src/opendirector/integrations/reference_inputs.py +++ b/packages/core/src/opendirector/integrations/reference_inputs.py @@ -108,6 +108,8 @@ def generation_references(store, task, config, *, complete=True, overrides=None) return result if kind == 'audio': if rules.get('referencesAllowed') is False and task['referenceIds']: + if rules['profile'] == 'minimax-speech': + raise ProjectError('MiniMax 语音合成不支持参考素材,请直接填写朗读文本并选择音色。', 422, code='minimax_speech_reference') raise ProjectError('MOSS 语音生成不支持参考素材,请直接填写朗读文本和音色 ID。', 422, code='moss_speech_reference') # Historical context remains recorded; it is never appended to speech. result['manifest'] = [{'assetId': meta['id'], 'fingerprint': meta['fingerprint'], 'kind': source_kind, 'transmission': 'context_only'} diff --git a/src/canvas/generationFailureText.ts b/src/canvas/generationFailureText.ts index 23246b64..24def45f 100644 --- a/src/canvas/generationFailureText.ts +++ b/src/canvas/generationFailureText.ts @@ -39,6 +39,7 @@ const BY_MESSAGE: Record = { "生成服务连接超时或中断。": "errors.generationFailure.temporary", "生成服务返回了无法识别或不完整的结果。": "errors.generationFailure.protocol", + "所选音色不存在或不可用。": "errors.generationFailure.voiceNotFound", }; const BY_CATEGORY: Record = { diff --git a/src/canvas/modelBrands.ts b/src/canvas/modelBrands.ts index 4cce35b4..18c7864e 100644 --- a/src/canvas/modelBrands.ts +++ b/src/canvas/modelBrands.ts @@ -12,6 +12,7 @@ export function modelBrand(modelId: string, explicitOrganization?: string) { if (exact) return exact; const aliases: [RegExp, string][] = [ [/^(minimax|fasth3|fastvideo-fasth3)/i, "MiniMaxAI"], + [/^speech-(?:2\.[68]|0[12])-(?:hd|turbo)$/i, "MiniMaxAI"], [/^kling(?:-|$)/i, "KlingAI"], [/^(deepseek)/i, "deepseek-ai"], [/^(qwen|qwq)/i, "Qwen"], diff --git a/src/canvas/providerPresets.ts b/src/canvas/providerPresets.ts index 08bea610..67f738c1 100644 --- a/src/canvas/providerPresets.ts +++ b/src/canvas/providerPresets.ts @@ -9,6 +9,7 @@ const EN_NAMES: Record = { "智谱 BigModel": "Zhipu BigModel", "可灵官方视频": "Kling official video", "MiniMax 官方图片": "MiniMax Official Images", + "MiniMax 官方语音": "MiniMax Official Speech", "MOSS 开放平台": "MOSS Open Platform", "火山方舟 / Agent Plan": "Volcano Ark / Agent Plan", "vLLM-Omni 图片接口": "vLLM-Omni image API", @@ -97,6 +98,17 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ authMode: "bearer", modelsByKind: { image: ["MiniMax-H3-Image", "MiniMax-H3-Image-Highspeed"] }, }, + { + id: "minimax-speech", + logoOrganization: "MiniMaxAI", + name: "MiniMax 官方语音", + kinds: ["audio"], + url: "https://api.minimax.cn/v1", + protocol: "openai", + connectionType: "api", + authMode: "bearer", + modelsByKind: { audio: ["speech-2.8-hd", "speech-2.8-turbo"] }, + }, { id: "minimax-video", logoOrganization: "MiniMaxAI", diff --git a/src/contracts/creation.schema.json b/src/contracts/creation.schema.json index 5ff5a4d7..37bddee1 100644 --- a/src/contracts/creation.schema.json +++ b/src/contracts/creation.schema.json @@ -3487,6 +3487,9 @@ "minimax_image_reference", "minimax_image_count", "minimax_image_bytes", + "minimax_speech_text", + "minimax_speech_voice", + "minimax_speech_reference", "voice_library_unsupported", "voice_connection_changed", "voice_reference_required", @@ -14387,7 +14390,8 @@ "description": "Voice registry protocol.", "enum": [ "vllm_omni", - "moss" + "moss", + "minimax" ], "title": "Provider", "type": "string" @@ -18653,6 +18657,9 @@ "minimax_image_reference", "minimax_image_count", "minimax_image_bytes", + "minimax_speech_text", + "minimax_speech_voice", + "minimax_speech_reference", "voice_library_unsupported", "voice_connection_changed", "voice_reference_required", @@ -30007,7 +30014,8 @@ "description": "Voice registry protocol.", "enum": [ "vllm_omni", - "moss" + "moss", + "minimax" ], "title": "Provider", "type": "string" diff --git a/src/contracts/generated.ts b/src/contracts/generated.ts index ebf5b9c8..58ad015f 100644 --- a/src/contracts/generated.ts +++ b/src/contracts/generated.ts @@ -387,7 +387,7 @@ export interface ContractInput { } export interface ContractIssueInput extends ContractInput { - readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; + readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "minimax_speech_text" | "minimax_speech_voice" | "minimax_speech_reference" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; readonly message: string; readonly objectId?: string | null; readonly field?: string | null; @@ -1529,7 +1529,7 @@ export interface ViewportInput extends PointInput { } export interface VoiceLibraryCapabilitiesInput extends ContractInput { - readonly provider: "vllm_omni" | "moss"; + readonly provider: "vllm_omni" | "moss" | "minimax"; readonly connectionId: string; readonly requiresTranscript: boolean; readonly requiresReferenceAudio: boolean; @@ -1959,7 +1959,7 @@ export interface Contract { } export interface ContractIssue extends Contract { - readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; + readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "minimax_speech_text" | "minimax_speech_voice" | "minimax_speech_reference" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; readonly message: string; readonly objectId: string | null; readonly field: string | null; @@ -3100,7 +3100,7 @@ export interface Viewport extends Point { } export interface VoiceLibraryCapabilities extends Contract { - readonly provider: "vllm_omni" | "moss"; + readonly provider: "vllm_omni" | "moss" | "minimax"; readonly connectionId: string; readonly requiresTranscript: boolean; readonly requiresReferenceAudio: boolean; diff --git a/src/i18n/messages/en-US/errors.ts b/src/i18n/messages/en-US/errors.ts index 9b43de92..d658aed2 100644 --- a/src/i18n/messages/en-US/errors.ts +++ b/src/i18n/messages/en-US/errors.ts @@ -139,7 +139,7 @@ export const errors = { "Enter a voice id supported by the service (letters, numbers, dots, underscores, or hyphens).", "errors.rules.voiceRequired": "Enter a valid Voice ID from the service's voice library before generating.", "errors.rules.referencesUnsupported": "This model does not support references. Disconnect the references before generating.", - "errors.generationFailure.voiceNotFound": "The voice was not found or is unavailable to this account. Check the Voice ID in the MOSS voice library.", + "errors.generationFailure.voiceNotFound": "The voice was not found or is unavailable to this account. Check the Voice ID in the voice library.", "errors.rules.audioDisable": "The video adapter does not support disabling generated audio.", "errors.rules.duration": diff --git a/src/i18n/messages/zh-CN/errors.ts b/src/i18n/messages/zh-CN/errors.ts index 77a9cc13..f689b6d7 100644 --- a/src/i18n/messages/zh-CN/errors.ts +++ b/src/i18n/messages/zh-CN/errors.ts @@ -99,7 +99,7 @@ export const errors = { "errors.rules.voiceId": "请输入服务支持的音色标识(字母、数字、点、下划线或连字符)。", "errors.rules.voiceRequired": "请先填写服务音色库中有效的音色 ID,再开始生成。", "errors.rules.referencesUnsupported": "当前模型不支持参考素材,请断开参考后再生成。", - "errors.generationFailure.voiceNotFound": "该音色不存在或当前账户无权使用,请在 MOSS 音色库核对音色 ID。", + "errors.generationFailure.voiceNotFound": "该音色不存在或当前账户无权使用,请在音色库核对音色 ID。", "errors.rules.audioDisable": "当前视频适配器不支持关闭生成音频。", "errors.rules.duration": "当前模型不支持所选时长,请重新选择。", "errors.rules.resolution": "当前模型或参考模式不支持所选分辨率,请重新选择。", diff --git a/tests/browser/voice_library.mjs b/tests/browser/voice_library.mjs index 82fcfd49..2102fd45 100644 --- a/tests/browser/voice_library.mjs +++ b/tests/browser/voice_library.mjs @@ -141,6 +141,10 @@ const validWebm = liveWebm(2); const longWebm = liveWebm(31); const copy = { "en-US": { + settings: "Settings", models: "Model connections", audioOverview: "Audio model", + addService: "Add service", provider: "Provider", speech: "MiniMax Official Speech", + url: "Service URL (Base URL)", saveService: "Save service", manage: "Manage", + cancel: "Cancel", closeSettings: "Close settings", tools: "Canvas creation tools", add: "Add Audio", model: "Generation model", @@ -175,6 +179,10 @@ const copy = { "The selected voice is no longer in this library. Choose another voice before generating.", }, "zh-CN": { + settings: "设置", models: "模型接入", audioOverview: "音频模型", + addService: "添加服务", provider: "提供商", speech: "MiniMax 官方语音", + url: "服务地址(Base URL)", saveService: "保存服务", manage: "管理", + cancel: "取消", closeSettings: "关闭设置", tools: "画布创作工具", add: "添加音频", model: "生成模型", @@ -220,7 +228,7 @@ try { page.on("pageerror", (error) => errors.push(error.message)); page.on("request", (request) => { if ( - /^https?:\/\/(voice\.test|voice-b\.test|api\.mosi\.cn)/.test( + /^https?:\/\/(voice\.test|voice-b\.test|api\.mosi\.cn|api\.minimax\.cn)/.test( request.url(), ) ) @@ -921,6 +929,70 @@ try { (await call("/api/creation-test-health")).calls.length, calls.length, ); + + await page.getByRole("button", { name: L.settings, exact: true }).click(); + await page.getByRole("button", { name: L.models, exact: true }).click(); + await page.locator(".cw-model-setup-row") + .filter({ has: page.locator("strong", { hasText: new RegExp(`^${L.audioOverview}$`) }) }) + .getByRole("button").click(); + const service = page.locator(".cw-model-service").filter({ hasText: "https://api.minimax.cn/v1" }); + if (await service.count()) { + await service.getByRole("button", { name: L.manage, exact: true }).click(); + assert.equal(await page.getByLabel("API Key", { exact: true }).inputValue(), ""); + } else { + await page.getByRole("button", { name: L.addService, exact: true }).click(); + await page.getByRole("combobox", { name: L.provider, exact: true }).click(); + await page.getByRole("option", { name: L.speech, exact: true }).click(); + await page.getByLabel("API Key", { exact: true }).fill("minimax-browser-fixture-only"); + } + assert.equal(await page.getByRole("combobox", { name: L.provider, exact: true }).innerText(), L.speech); + assert.equal(await page.getByLabel(L.url, { exact: true }).inputValue(), "https://api.minimax.cn/v1"); + for (const model of ["speech-2.8-hd", "speech-2.8-turbo"]) + assert.equal(await page.getByRole("checkbox", { name: model, exact: true }).isChecked(), true); + for (const logo of await page.locator('.cw-model-option img[title="MiniMax"]').all()) { + await logo.evaluate(element => element.decode()); + assert.ok(await logo.evaluate(element => element.naturalWidth > 0)); + } + assert.equal(await page.locator('.cw-model-option img[title="MiniMax"]').count(), 2); + await page.screenshot({ path: `${output}/${locale}-minimax-provider.png`, fullPage: true }); + await page.getByRole("button", { name: L.saveService, exact: true }).click(); + await page.getByRole("button", { name: L.addService, exact: true }).waitFor(); + const minimax = (await call("/api/model-configs")).filter(model => model.inputRules.profile === "minimax-speech"); + assert.equal(minimax.length, 2); + assert.equal(new Set(minimax.map(model => model.inputRules.voiceLibrary.connectionId)).size, 1); + assert.ok(!JSON.stringify(minimax).includes("minimax-browser-fixture-only")); + await page.getByRole("button", { name: L.closeSettings, exact: true }).click(); + await selectModel(minimax[0]); + assert.equal((await params()).voiceSelection, undefined); + assert.equal(await page.getByRole("button", { name: L.generate, exact: true }).isDisabled(), true); + await open(); + await dialog().getByRole("button", { name: "Santa Claus", exact: true }).waitFor(); + assert.equal(await dialog().getByRole("button", { name: L.create, exact: true }).isDisabled(), true); + assert.equal(await dialog().getByRole("button", { name: L.delete, exact: true }).count(), 0); + await page.screenshot({ path: `${output}/${locale}-minimax-voices.png`, fullPage: true }); + await dialog().getByRole("button", { name: "Santa Claus", exact: true }).click(); + await until(async () => (await params()).voice === "Santa_Claus "); + await selectModel(minimax[1]); + assert.equal((await params()).voiceSelection.voiceId, "Santa_Claus "); + await page.getByRole("textbox", { name: L.prompt, exact: true }).fill("🎙".repeat(10000)); + assert.equal(await page.getByRole("button", { name: L.generate, exact: true }).isDisabled(), true); + await page.getByRole("textbox", { name: L.prompt, exact: true }).fill("Hello from MiniMax speech."); + const beforeMinimax = (await state()).jobs.map(job => job.id); + await page.getByRole("button", { name: L.generate, exact: true }).click(); + const speechJob = await until(async () => (await state()).jobs.find(job => + !beforeMinimax.includes(job.id) && job.status === "succeeded")); + assert.equal(speechJob.inputSnapshot.spec.parameters.voiceSelection.voiceId, "Santa_Claus "); + assert.equal(speechJob.execution.voice, "Santa_Claus "); + const speechAsset = (await state()).assets.find(asset => asset.id === speechJob.asset.assetId); + const speechPath = `/api/creation/projects/${id}/assets/${speechAsset.id}/content`; + assert.ok((await reopenAudio(page, speechPath, L.saved)).ok()); + await page.locator(".react-flow__node").first().click(); + assert.match(await page.getByRole("button", { name: L.library, exact: true }).innerText(), /Santa Claus/); + await page.waitForFunction(() => document.querySelector(".cw-audio-preview audio")?.readyState > 0); + await player.evaluate(audio => audio.play()); + await page.waitForFunction(() => document.querySelector(".cw-audio-preview audio")?.ended); + assert.ok(await player.evaluate(audio => audio.currentTime > 0 && audio.error === null)); + await page.screenshot({ path: `${output}/${locale}-minimax-played.png`, fullPage: true }); } catch (error) { await page .screenshot({ path: `${output}/${locale}-failure.png`, fullPage: true }) diff --git a/tests/browser/voice_library_server.py b/tests/browser/voice_library_server.py index 8cb8a543..94c95b5e 100644 --- a/tests/browser/voice_library_server.py +++ b/tests/browser/voice_library_server.py @@ -4,6 +4,7 @@ pass real ffprobe validation, and generation still validates the voice registry. """ import io +import json from email.parser import BytesParser from email.policy import default from pathlib import Path @@ -25,6 +26,13 @@ def provider(request): calls.append(dict(method=request.method, url=str(request.url))) + if request.url.host == 'api.minimax.cn': + assert request.method == 'POST' and request.url.path == '/v1/get_voice' + assert json.loads(request.content) == {'voice_type': 'system'} + return httpx.Response(200, json={'base_resp': {'status_code': 0}, 'system_voice': [ + {'voice_id': 'Chinese (Mandarin)_Reliable_Executive', 'voice_name': 'Narrator'}, + {'voice_id': 'Santa_Claus ', 'voice_name': 'Santa Claus'}, + ]}) cloud = request.url.host == 'api.mosi.cn' boundary = request.url.host records = uploads.setdefault(boundary, {}) diff --git a/tests/generationFailureText.test.ts b/tests/generationFailureText.test.ts index 362c9b85..e71fa00b 100644 --- a/tests/generationFailureText.test.ts +++ b/tests/generationFailureText.test.ts @@ -126,10 +126,10 @@ test("voice_not_found identifies the voice before generic 404 or input failures" try { setActiveLocale("en-US"); assert.equal(generationFailureMessage(failure({ providerCode: "voice_not_found", httpStatus: 404 })), - "The voice was not found or is unavailable to this account. Check the Voice ID in the MOSS voice library."); + "The voice was not found or is unavailable to this account. Check the Voice ID in the voice library."); setActiveLocale("zh-CN"); assert.equal(generationFailureMessage(failure({ providerCode: "voice_not_found", httpStatus: 404 })), - "该音色不存在或当前账户无权使用,请在 MOSS 音色库核对音色 ID。"); + "该音色不存在或当前账户无权使用,请在音色库核对音色 ID。"); } finally { setActiveLocale("zh-CN"); } @@ -153,3 +153,15 @@ test("provider moderation verdicts are explained in both languages", () => { /生成内容涉及敏感信息/, ); }); + +test("MiniMax missing voices use the shared localized message", () => { + const value = failure({ providerCode: "2054", httpStatus: 200, message: "所选音色不存在或不可用。" }); + try { + setActiveLocale("en-US"); + assert.match(generationFailureMessage(value), /voice library/); + assert.doesNotMatch(generationFailureMessage(value), /MOSS|[\u3400-\u9fff]/); + setActiveLocale("zh-CN"); + assert.match(generationFailureMessage(value), /音色库/); + assert.doesNotMatch(generationFailureMessage(value), /MOSS/); + } finally { setActiveLocale("zh-CN"); } +}); diff --git a/tests/modelBrands.test.ts b/tests/modelBrands.test.ts index b82ce537..9edfab38 100644 --- a/tests/modelBrands.test.ts +++ b/tests/modelBrands.test.ts @@ -8,6 +8,9 @@ import { modelBrand } from "../src/canvas/modelBrands.ts"; test("model brands recognize organizations and known bare names without assigning unknown aliases", () => { assert.equal(modelBrand("MiniMaxAI/MiniMax-H3")?.org, "MiniMaxAI"); assert.equal(modelBrand("MiniMax-H3")?.org, "MiniMaxAI"); + assert.equal(modelBrand("speech-2.8-hd")?.org, "MiniMaxAI"); + assert.equal(modelBrand("speech-02-turbo")?.org, "MiniMaxAI"); + assert.equal(modelBrand("speech-custom"), undefined); assert.equal(modelBrand("kling-4.0")?.file, "kling.svg"); assert.equal(modelBrand("kling-4.0-flash")?.file, "kling.svg"); assert.equal(modelBrand("deepseek-ai/DeepSeek-V3")?.name, "DeepSeek"); diff --git a/tests/providerPresets.test.ts b/tests/providerPresets.test.ts index 6c66bf57..1209d564 100644 --- a/tests/providerPresets.test.ts +++ b/tests/providerPresets.test.ts @@ -175,3 +175,21 @@ test("MiniMax H3 image preset exposes both models in both display languages", () setActiveLocale(previous); } }); + +test("MiniMax speech preset and saved identity follow the display language", () => { + const previous = getActiveLocale(); + try { + const preset = PROVIDER_PRESETS.find(item => item.id === "minimax-speech")!; + assert.equal(preset.url, "https://api.minimax.cn/v1"); + assert.deepEqual(preset.kinds, ["audio"]); + assert.deepEqual(preset.modelsByKind?.audio, ["speech-2.8-hd", "speech-2.8-turbo"]); + for (const [locale, name] of [["zh-CN", "MiniMax 官方语音"], ["en-US", "MiniMax Official Speech"]] as const) { + setActiveLocale(locale); + assert.equal(providerOptions("audio").find(item => item.id === preset.id)?.name, name); + const model: ModelConfig = { ...preset, id: "saved-minimax", kind: "audio", modelId: "speech-2.8-hd", + inputRules: { profile: "minimax-speech", allowedKinds: [], maxImages: 0, maxVideos: 0, maxAudios: 0, videoModes: [] } }; + assert.equal(providerPresetFor(model), preset.id); + assert.equal(providerPresetFor({ ...model, url: "https://api.minimax.io/v1" }), "custom"); + } + } finally { setActiveLocale(previous); } +}); diff --git a/tests/test_minimax_speech.py b/tests/test_minimax_speech.py new file mode 100644 index 00000000..513da1d6 --- /dev/null +++ b/tests/test_minimax_speech.py @@ -0,0 +1,374 @@ +"""MiniMax T2A wire contract and real MP3 persistence with mocked provider HTTP.""" +import asyncio +import json +from uuid import uuid4 + +import httpx +import pytest +from fastapi.testclient import TestClient + +from generation_helpers import command, prepared, settled +from opendirector.agent.tools import AgentTools +from opendirector.application.creation_assets import CreationAssets +from opendirector.application.creation_canvas import asset_ref +from opendirector.application.creation_generation import CreationGeneration +from opendirector.application.creation_tool_assets import CreationToolAssets +from opendirector.application.voice_library import VoiceLibrary +from opendirector.domain.capabilities import SpeechRequest +from opendirector.domain.creation.generation import ResolvedReference, validate_generation +from opendirector.domain.creation.models import ModelConfigPublic +from opendirector.domain.generation import GenerationError +from opendirector.domain.input_rules import input_rules +from opendirector.domain.projects import ProjectError +from opendirector.infrastructure.creation_projects import CreationProjects +from opendirector.infrastructure.local_projects import LocalProjects +from opendirector.infrastructure.model_configs import ModelConfigs +from opendirector.integrations.model_adapters import MinimaxSpeech, OpenAISpeech, adapter_for +from opendirector.integrations.providers import GenerationProviders +from opendirector.integrations.reference_inputs import generation_references +from opendirector_api.main import create_app +from test_creation_canvas_service import project +from test_creation_generation_service import anyio_backend +from test_moss import mp3 + +URL = 'https://api.minimax.cn/v1' +RESULT = 'https://93.184.215.14/speech.mp3?signature=fixture-result-signature' +SECRET = 'minimax-test-key' +VOICE = 'Chinese (Mandarin)_Reliable_Executive' +CONFIG = dict(kind='audio', url=URL, modelId='speech-2.8-hd', apiKey=SECRET, + protocol='openai', connectionType='api', authMode='bearer') +SUCCESS = dict(base_resp={'status_code': 0, 'status_msg': 'success'}, data={'audio': RESULT, 'status': 2}) +VOICES = dict(base_resp={'status_code': 0}, system_voice=[ + {'voice_id': VOICE, 'voice_name': 'Narrator'}, {'voice_id': 'Santa_Claus ', 'voice_name': 'Santa Claus'}, +]) + + +@pytest.fixture +def context(project, tmp_path): + configs = ModelConfigs(tmp_path) + saved = configs.save('audio', URL, CONFIG['modelId'], SECRET, protocol='openai') + return project, configs, saved + + +def runner_for(context, handle): + project, configs, _ = context + runner = CreationGeneration(project[0], configs) + transport = httpx.MockTransport(handle) + runner.provider = GenerationProviders(runner.store, transport, retry_delays=(0, 0)) + runner.voices = VoiceLibrary(configs, transport) + return runner + + +def selection(saved, voice=VOICE): + return dict(voice=voice, voiceSelection=dict(voiceId=voice, name='Narrator', source='builtin', + hasReferenceAudio=False, connectionId=saved['inputRules']['voiceLibrary']['connectionId'])) + + +@pytest.mark.parametrize('model', ['speech-2.8-hd', 'speech-2.8-turbo', 'speech-2.6-hd', + 'speech-2.6-turbo', 'speech-02-hd', 'speech-02-turbo', 'speech-01-hd', 'speech-01-turbo']) +def test_official_models_have_read_only_voice_library_and_shared_rules(model): + config = {**CONFIG, 'modelId': model, 'url': URL + '/'} + rules = input_rules(config) + assert isinstance(adapter_for(config), MinimaxSpeech) + assert rules['profile'] == 'minimax-speech' + assert rules['voiceRequired'] and rules['referencesAllowed'] is False + assert rules['maxPromptCharacters'] == 9999 and rules['allowedKinds'] == [] + library = rules['voiceLibrary'] + assert library['provider'] == 'minimax' + assert not library['supportsUploadedVoices'] and not library['canDelete'] + assert not library['requiresReferenceAudio'] and not library['requiresTranscript'] + + +@pytest.mark.parametrize('patch', [ + {'url': 'https://api.minimax.cn.evil.test/v1'}, {'url': 'https://api.minimax.io/v1'}, + {'url': 'http://api.minimax.cn/v1'}, {'url': 'https://api.minimax.cn:8443/v1'}, + {'url': 'https://api.minimax.cn/v2'}, {'modelId': 'speech-2.8-unknown'}, + {'connectionType': 'self_hosted'}, +]) +def test_other_services_and_lookalikes_keep_the_generic_adapter(patch): + config = {**CONFIG, **patch} + assert isinstance(adapter_for(config), OpenAISpeech) + assert input_rules(config)['profile'] != 'minimax-speech' + + +def test_system_voices_preserve_ids_and_account_scope(context): + project, configs, saved = context + calls = [] + def handle(request): + calls.append(request) + assert request.method == 'POST' and str(request.url) == URL + '/get_voice' + assert json.loads(request.content) == {'voice_type': 'system'} + assert request.headers['authorization'] == 'Bearer ' + SECRET + return httpx.Response(200, json=VOICES) + library = VoiceLibrary(configs, httpx.MockTransport(handle)) + listed = library.list(saved['id']) + assert [voice['voiceId'] for voice in listed['voices']] == [VOICE, 'Santa_Claus '] + assert listed['nextCursor'] is None + assert all(voice['compatible'] and voice['source'] == 'builtin' and not voice['canDelete'] + and voice['previewUrl'] is None for voice in listed['voices']) + private = configs.resolve(saved['id'], 'audio') + library.validate_selection(private, 'Santa_Claus ', listed['voices'][1]) + with pytest.raises(ProjectError) as failure: + library.validate_selection(private, VOICE, {**listed['voices'][0], 'connectionId': 'stale'}) + assert failure.value.code == 'voice_connection_changed' + with pytest.raises(ProjectError) as failure: + library.create(saved['id'], b'', filename='voice.wav', name='Test') + assert failure.value.code == 'voice_model_unsupported' + with pytest.raises(ProjectError) as failure: + library.delete(saved['id'], VOICE, listed['connectionId']) + assert failure.value.code == 'voice_delete_unsupported' + assert len(calls) == 2 + assert SECRET not in json.dumps(saved) + json.dumps(listed) + business = AgentTools(project[0], None, configs) + business.voices = library + assert asyncio.run(business.execute(project[1], uuid4(), project[2], 'voice-list', 'voice_list', + {'modelConfigId': saved['id']}, [], None)) == listed + + +@pytest.mark.parametrize('body', [{}, {'base_resp': {'status_code': False}, 'system_voice': []}, + {'base_resp': {'status_code': 0}, 'system_voice': None}, + {'base_resp': {'status_code': 0}, 'system_voice': [{'voice_id': '../voice'}]}, + {'base_resp': {'status_code': 0}, 'system_voice': [VOICES['system_voice'][0]] * 2}]) +def test_voice_library_rejects_malformed_catalog(context, body): + _, configs, saved = context + library = VoiceLibrary(configs, httpx.MockTransport(lambda _: httpx.Response(200, json=body))) + with pytest.raises(ProjectError) as failure: + library.list(saved['id']) + assert failure.value.code == 'voice_provider_protocol' + + +def test_voice_library_error_does_not_echo_provider_secrets(context): + _, configs, saved = context + body = {'base_resp': {'status_code': 1004, 'status_msg': SECRET + RESULT}} + library = VoiceLibrary(configs, httpx.MockTransport(lambda _: httpx.Response(200, json=body))) + with pytest.raises(ProjectError) as failure: + library.list(saved['id']) + assert failure.value.code == 'voice_provider_error' + assert SECRET not in str(failure.value) and 'fixture-result-signature' not in str(failure.value) + + +@pytest.mark.anyio +@pytest.mark.parametrize('voice', [VOICE, 'Santa_Claus ']) +async def test_exact_wire_request_checkpoint_and_credential_free_download(mp3, voice): + calls, checkpoints = [], [] + async def ready(suffix, url): + checkpoints.append((suffix, url)) + def handle(request): + calls.append(request) + if request.method == 'POST': + assert str(request.url) == URL + '/t2a_v2' + assert request.headers['authorization'] == 'Bearer ' + SECRET + assert json.loads(request.content) == dict(model='speech-2.8-hd', text='🎙' * 9999, + stream=False, voice_setting={'voice_id': voice}, audio_setting={'format': 'mp3'}, output_format='url') + return httpx.Response(200, json=SUCCESS) + assert checkpoints == [('mp3', RESULT)] + assert str(request.url) == RESULT and 'authorization' not in request.headers + return httpx.Response(200, content=mp3) + adapter = adapter_for(CONFIG, httpx.MockTransport(handle)) + result = await adapter.speech(CONFIG, SpeechRequest('🎙' * 9999, voice), result_ready=ready) + assert result.data == mp3 and result.suffix == 'mp3' + assert [call.method for call in calls] == ['POST', 'GET'] + + +@pytest.mark.anyio +@pytest.mark.parametrize('body', [{}, {'base_resp': {'status_code': False}, 'data': SUCCESS['data']}, + {'base_resp': {'status_code': 0}, 'data': None}, + {'base_resp': {'status_code': 0}, 'data': {'audio': RESULT, 'status': 1}}, + {'base_resp': {'status_code': 0}, 'data': {'audio': 'file:///tmp/voice.mp3', 'status': 2}}, + {'base_resp': {'status_code': 0}, 'data': {'audio': '494433', 'status': 2}}]) +async def test_malformed_or_incomplete_submission_is_unknown_and_never_reposted(body): + calls = [] + def handle(request): + calls.append(request) + return httpx.Response(200, json=body) + with pytest.raises(GenerationError) as failure: + await MinimaxSpeech(httpx.MockTransport(handle)).speech(CONFIG, SpeechRequest('Read this', VOICE)) + assert failure.value.details.category == 'submission_unknown' + assert len(calls) == 1 and calls[0].method == 'POST' + + +@pytest.mark.anyio +@pytest.mark.parametrize(('code', 'category'), [(1004, 'authentication'), (1008, 'quota'), + (1002, 'rate_limit'), (2013, 'input'), (2054, 'input'), (1026, 'input'), (1001, 'temporary')]) +async def test_http_200_provider_errors_are_safe_and_not_retried(code, category): + calls = [] + def handle(request): + calls.append(request) + return httpx.Response(200, json={'base_resp': {'status_code': code, 'status_msg': SECRET + RESULT}, + 'trace_id': 'trace-fixture-1'}) + with pytest.raises(GenerationError) as failure: + await MinimaxSpeech(httpx.MockTransport(handle)).speech(CONFIG, SpeechRequest('Read this', VOICE)) + details = failure.value.details + assert details.category == category and details.providerCode == str(code) + assert details.requestId == 'trace-fixture-1' and len(calls) == 1 + assert SECRET not in details.model_dump_json() and 'fixture-result-signature' not in details.model_dump_json() + + +@pytest.mark.anyio +async def test_submission_timeout_never_reposts(): + calls = [] + def handle(request): + calls.append(request) + raise httpx.ReadTimeout('unknown acceptance', request=request) + with pytest.raises(GenerationError) as failure: + await MinimaxSpeech(httpx.MockTransport(handle)).speech(CONFIG, SpeechRequest('Read this', VOICE)) + assert failure.value.details.category == 'submission_unknown' and len(calls) == 1 + + +@pytest.mark.anyio +@pytest.mark.parametrize(('text', 'voice'), [('', VOICE), (' \n', VOICE), ('Read this', None), + ('Read this', 'bad\nvoice'), ('Read this', ' '), ('x' * 10000, VOICE)], + ids=['empty', 'blank', 'missing-voice', 'control-character', 'blank-voice', 'text-limit']) +async def test_direct_adapter_rejects_invalid_inputs_without_http(text, voice): + adapter = MinimaxSpeech(httpx.MockTransport(lambda _: pytest.fail('invalid input reached HTTP'))) + with pytest.raises(ProjectError): + await adapter.speech(CONFIG, SpeechRequest(text, voice)) + + +@pytest.mark.anyio +@pytest.mark.parametrize('url', ['http://127.0.0.1/voice.mp3', 'http://169.254.169.254/latest/meta-data/', + 'http://[::1]/voice.mp3']) +async def test_result_download_rejects_private_addresses(url): + adapter = MinimaxSpeech(httpx.MockTransport(lambda _: pytest.fail('private destination reached HTTP'))) + with pytest.raises(GenerationError): + await adapter.download(CONFIG, url) + + +@pytest.mark.parametrize('source', ['ui', 'agent']) +@pytest.mark.parametrize(('prompt', 'voice', 'code'), [('Read this', None, 'minimax_speech_voice'), + ('🎙' * 10000, VOICE, 'minimax_speech_text')], ids=['missing-voice', 'text-limit']) +def test_preflight_rejects_input_before_http_or_job(context, source, prompt, voice, code): + project, _, saved = context + runner = runner_for(context, lambda _: pytest.fail('invalid input reached HTTP')) + request = command(project, saved, prompt=prompt, parameters=selection(saved, voice) if voice else {}) + before = project[0].snapshot(project[1]) + receipt = runner.prepare(request, session=project[2], source=source) + assert receipt.status == 'rejected' and receipt.issues[0].code == code + assert not receipt.jobIds and project[0].snapshot(project[1]) == before + assert runner.prepare(request, session=project[2], source=source) == receipt + + +@pytest.mark.parametrize('transmission', ['content', 'context_only']) +def test_reference_boundaries_reject_even_context_only_inputs(context, transmission): + project, _, saved = context + store, pid, session = project + asset = CreationToolAssets(store).import_file(str(pid), str(uuid4()), 'note.md', b'Reference words', session) + runner = runner_for(context, lambda _: pytest.fail('references reached HTTP')) + request = command(project, saved, parameters=selection(saved)) + snapshot = store.snapshot(pid) + spec = next(item for item in snapshot.generationSpecs if item.id == request.payload.specId) + reference = ResolvedReference(referenceId=uuid4(), role='reference', asset=asset_ref(snapshot.assets[0]), + metadata=snapshot.assets[0].metadata, transmission=transmission) + with pytest.raises(ValueError, match='参考'): + validate_generation(spec, ModelConfigPublic.model_validate(saved), (reference,)) + with pytest.raises(ProjectError) as failure: + generation_references(runner.store, dict(kind='audio', projectId=str(pid), prompt='Read this', + referenceIds=[asset['id']], parameters={}), CONFIG) + assert failure.value.code == 'minimax_speech_reference' and not store.snapshot(pid).jobs + + +def test_http_receipt_replay_switches_language_without_changing_storage(context): + project, _, saved = context + store, pid, session = project + request = command(project, saved, prompt='Read this') + headers = {'X-OpenDirector-Request': '1', 'X-OpenDirector-Session': session} + path = f'/api/creation/projects/{pid}' + with TestClient(create_app(store.local), headers=headers) as client: + response = client.post(path + '/commands', json=request.model_dump(mode='json'), headers={'Accept-Language': 'en-US'}) + assert response.status_code == 200 + receipt = response.json() + assert receipt['issues'][0]['code'] == 'minimax_speech_voice' + assert receipt['issues'][0]['message'] == 'Choose a voice available to your MiniMax account from the voice library.' + assert client.get(path + f'/receipts/{request.commandId}', headers={'Accept-Language': 'en-US'}).json() == receipt + chinese = client.get(path + f'/receipts/{request.commandId}', headers={'Accept-Language': 'zh-CN'}).json() + assert '音色' in chinese['issues'][0]['message'] + assert chinese == store.receipt(pid, request.commandId).model_dump(mode='json') + assert not store.snapshot(pid).jobs + + +@pytest.mark.anyio +@pytest.mark.parametrize('source', ['ui', 'agent']) +async def test_real_mp3_snapshot_adoption_replay_and_reopen(context, mp3, source): + project, _, saved = context + store, pid, session = project + calls = [] + def handle(request): + calls.append(request) + if request.url.path == '/v1/get_voice': return httpx.Response(200, json=VOICES) + if request.method == 'POST': return httpx.Response(200, json=SUCCESS) + return httpx.Response(200, content=mp3) + runner = runner_for(context, handle) + try: + request = command(project, saved, prompt='Read this', parameters=selection(saved, 'Santa_Claus ')) + receipt = await runner.execute(request, session=session, source=source) + assert receipt.status == 'accepted' + await settled(runner) + snapshot = store.snapshot(pid) + [job] = snapshot.jobs + assert job.status == 'succeeded' and job.adopted + assert job.inputSnapshot.spec.parameters.voice == job.execution.voice == 'Santa_Claus ' + assert job.execution.format == 'mp3' + assert job.adapterParameters == dict(voice='Santa_Claus ', format='mp3', output_format='url', stream=False) + [asset] = snapshot.assets + assert asset.metadata.mimeType == 'audio/mpeg' and asset.metadata.durationSeconds > 0 + stream, _ = CreationAssets(store).open_media(pid, asset.id) + with stream: assert stream.read() == mp3 + assert await runner.execute(request, session=session, source=source) == receipt + assert sum(call.url.path == '/v1/t2a_v2' for call in calls) == 1 + assert SECRET not in snapshot.model_dump_json() and 'fixture-result-signature' not in snapshot.model_dump_json() + finally: + await runner.close() + assert CreationProjects(LocalProjects(store.local.repository)).snapshot(pid) == snapshot + + +@pytest.mark.anyio +async def test_failed_download_recovers_after_model_removal_without_resubmitting(context, mp3): + project, configs, saved = context + store, pid, session = project + calls, healthy = [], False + def handle(request): + calls.append(request) + if request.url.path == '/v1/get_voice': return httpx.Response(200, json=VOICES) + if request.method == 'POST': return httpx.Response(200, json=SUCCESS) + assert str(request.url) == RESULT and 'authorization' not in request.headers + if not healthy: raise httpx.ReadTimeout('download outage', request=request) + return httpx.Response(200, content=mp3) + first = runner_for(context, handle) + try: + first._launch(*prepared(first, project, saved, prompt='Read this', parameters=selection(saved))) + await settled(first) + [failed] = store.snapshot(pid).jobs + assert failed.recovery == 'download' and failed.resumeAllowed + assert not store.snapshot(pid).assets + finally: + await first.close() + configs.delete(saved['id']) + healthy = True + reopened = CreationProjects(LocalProjects(store.local.repository)) + restarted = runner_for(((reopened, pid, session), configs, saved), handle) + try: + await restarted.start() + await settled(restarted) + snapshot = reopened.snapshot(pid) + assert snapshot.jobs[0].id == failed.id and snapshot.jobs[0].status == 'succeeded' + assert len(snapshot.assets) == len(snapshot.results) == 1 + assert sum(call.url.path == '/v1/t2a_v2' for call in calls) == 1 + finally: + await restarted.close() + + +@pytest.mark.anyio +async def test_invalid_mp3_never_publishes_an_asset(context): + project, _, saved = context + def handle(request): + if request.url.path == '/v1/get_voice': return httpx.Response(200, json=VOICES) + if request.method == 'POST': return httpx.Response(200, json=SUCCESS) + return httpx.Response(200, content=b'ID3not-decodable-mp3') + runner = runner_for(context, handle) + try: + await runner.execute(command(project, saved, parameters=selection(saved)), session=project[2]) + await settled(runner) + snapshot = project[0].snapshot(project[1]) + assert snapshot.jobs[0].status == 'failed' and not snapshot.assets and not snapshot.results + finally: + await runner.close() From 6e52ca0dee00616983a2e01f8ca50c291992317b Mon Sep 17 00:00:00 2001 From: djj <421774554@qq.com> Date: Wed, 11 Nov 2026 11:10:45 +0800 Subject: [PATCH 2/2] [bugfix] providers: preserve MiniMax recovery and model routing ## summary - Recover downloads with the accepted model snapshot and no credentials, preserving public-host checks after configuration removal or replacement. - Reject unsupported official speech models through shared input rules, with bilingual errors, atomic save coverage and updated contracts. - Acknowledge session release before the persisted-audio browser check reloads, avoiding dependence on unload-time request delivery. ## validation - 70 MiniMax tests, 239 related Python tests and 23 frontend tests passed. - Bilingual voice workflow passed with Playwright 1.58.2. - Build, i18n, browser manifests, import boundaries and contracts passed. - Provider HTTP was mocked; live MiniMax synthesis was not checked. --- apps/api/src/opendirector_api/localization.py | 1 + docs/development/lessons.md | 1 + docs/development/validation.md | 4 +- docs/guides/user-guide.md | 2 +- docs/guides/user-guide.zh-CN.md | 2 +- .../application/creation_generation.py | 6 +- .../opendirector/domain/creation/commands.py | 2 +- .../src/opendirector/domain/input_rules.py | 5 +- .../src/opendirector/domain/minimax_speech.py | 2 +- src/contracts/creation.schema.json | 2 + src/contracts/generated.ts | 4 +- tests/browser/voice_library.mjs | 15 ++++ tests/test_minimax_speech.py | 89 ++++++++++++++++++- 13 files changed, 120 insertions(+), 15 deletions(-) diff --git a/apps/api/src/opendirector_api/localization.py b/apps/api/src/opendirector_api/localization.py index a31dcaf0..0a12ea6d 100644 --- a/apps/api/src/opendirector_api/localization.py +++ b/apps/api/src/opendirector_api/localization.py @@ -51,6 +51,7 @@ "minimax_image_count": "MiniMax H3 image editing accepts up to 16 reference images.", "minimax_image_bytes": "MiniMax H3 image reference requests cannot exceed 64 MiB.", "minimax_speech_text": "Enter the text to read aloud, up to 9,999 characters.", + "minimax_speech_model": "Unsupported MiniMax speech model. Choose a model from the preset.", "minimax_speech_voice": "Choose a voice available to your MiniMax account from the voice library.", "minimax_speech_reference": "MiniMax speech synthesis does not accept references. Enter the text to read aloud and choose a voice.", "moss_image_size": "MOSS image dimensions must be 256–2048, multiples of 16, and no more than 2,097,152 pixels in total.", diff --git a/docs/development/lessons.md b/docs/development/lessons.md index 13e6dfac..629933ad 100644 --- a/docs/development/lessons.md +++ b/docs/development/lessons.md @@ -65,6 +65,7 @@ When work exposes a consequential risk or a recurring problem, add a lesson or u - **Trigger:** Accepting a cross-origin result URL from a configured model service. - **Lesson:** [PR #210](https://github.com/ThinkFlowLab/OpenDirector/pull/210) added compatible image CDN downloads without provider credentials, but initially left arbitrary local and private hosts reachable. Credential isolation and bounded responses do not constrain the network destination. - **Check:** Preserve explicitly configured local service origins; validate all DNS answers for unconfigured hosts, connect to a checked public IP while retaining the original Host and TLS identity, and revalidate every redirect. Cover local metadata addresses, IPv6, mixed DNS answers, redirect loops, and recovery without another model submission. +- **Recovery follow-up:** [PR #283](https://github.com/ThinkFlowLab/OpenDirector/pull/283) reproduced download recovery losing its adapter's public-host checks after a model was removed or changed. Recover with the accepted model snapshot and no credentials instead of an untyped download. Test private URLs, changed DNS answers and redirects after configuration removal or replacement. ## An acknowledged editor save invalidates earlier automatic callbacks diff --git a/docs/development/validation.md b/docs/development/validation.md index cecbf452..b25a4269 100644 --- a/docs/development/validation.md +++ b/docs/development/validation.md @@ -281,8 +281,10 @@ CREATION_API_URL=http://127.0.0.1:8030 CREATION_WEB_URL=http://127.0.0.1:5190 no For a slow-response regression, set `VOICE_REOPEN_DELAY_MS=17000` for the browser command. It delays the reopened asset request beyond the ordinary 15-second interaction timeout. Cold navigation, saved-state and reopened-media waits have explicit 45-second limits; HTTP errors still fail immediately with their status. No generation is retried. +The audio-reopen check waits for the saved indicator and acknowledges the page's session-release request before reloading. Its result must not depend on an unload-time request reaching the server; keep the server's editing-lease checks intact. + 慢响应回归:在浏览器命令的环境变量中设置 `VOICE_REOPEN_DELAY_MS=17000`,让重开后的资源请求超过普通交互的 15 秒等待。冷启动导航、保存状态和重开媒体分别有明确的 45 秒上限;HTTP 错误仍立即报告状态并失败,不重试生成。 The bilingual browser workflow covers opening/outside click/Escape, keyboard tabs, four-column and narrow layouts, real previews and unavailable previews, metadata filters, scoped favorites, loading/error/retry, invalid and valid local audio, creation, selection, Drama3 transcript and Nano reference requirements, supported deletion and failure feedback, model/service switching, project reopening and persisted job input. `moss_speech.mjs` is retained as an alias for this integrated workflow. The pagination regression includes a 15,001-voice catalog. With a configured MOSS account, open the library, go to the next page, and return to the first. Verify the page counts and that returning to the first page restores its entries using read-only requests. Python tests verify provider fields, pagination, credentials, account boundaries, actual audio/container/duration validation, and equivalent Agent operations. Actual provider synthesis still requires a configured service, account permission and a recording authorized for use. -The same browser workflow also covers MiniMax's mainland speech preset in both languages, saved credentials remaining private, read-only system voices, IDs containing spaces, selection retention between HD and Turbo, the 9,999-character precheck, and generation/reopen/playback. Its generated audio is a local WAV fixture, not MiniMax output. `test_minimax_speech.py` uses mocked HTTP with real MP3 bytes to check `/t2a_v2` and `/get_voice`, HTTP-200 error envelopes, invalid or incomplete responses, public result downloads without credentials, UI/Agent acceptance and receipts, actual media probing, and recovery after model removal without another synthesis call. Live MiniMax synthesis and perceptual voice quality require a separately authorized real-account run. +The same browser workflow also covers MiniMax's mainland speech preset in both languages, localized rejection of unsupported model IDs, saved credentials remaining private, read-only system voices, IDs containing spaces, selection retention between HD and Turbo, the 9,999-character precheck, and generation/reopen/playback. Its generated audio is a local WAV fixture, not MiniMax output. `test_minimax_speech.py` uses mocked HTTP with real MP3 bytes to check `/t2a_v2` and `/get_voice`, HTTP-200 error envelopes, invalid or incomplete responses, public result downloads without credentials, UI/Agent acceptance and receipts, actual media probing, and recovery after model removal or replacement without another synthesis call. Recovery must still reject private destinations, DNS changes to private addresses, and redirects into private networks. Live MiniMax synthesis and perceptual voice quality remain separate real-account checks. diff --git a/docs/guides/user-guide.md b/docs/guides/user-guide.md index cfc56a49..0cebb147 100644 --- a/docs/guides/user-guide.md +++ b/docs/guides/user-guide.md @@ -91,7 +91,7 @@ For audio generation, enter the text to be read aloud in the prompt. Supported v On MOSS Open Platform, select the stable `moss-tts-1.5-flash` or `moss-tts-1.0-pro` model, or choose its dated snapshot to pin a version. All four use the same voice library on a given account connection. The library shows up to 150 MOSS voices per page. Use **Next page** and **Previous page** to browse. Filters and the Favorites tab show matches on the current page. -For MiniMax, choose **Settings → Model connections → Audio → Add service → MiniMax Official Speech**, enter your mainland China API key, and save `speech-2.8-hd` or `speech-2.8-turbo`. The preset uses `https://api.minimax.cn/v1`. In an audio node, select that model, choose a system voice from **Voice library**, and enter up to 9,999 characters. Reference assets, voice cloning, voice design, and deletion are not supported by this integration. The system voice API does not provide previews; selecting a voice does not start a paid synthesis request. Compatible MiniMax speech models on the same account connection retain the selection. +For MiniMax, choose **Settings → Model connections → Audio → Add service → MiniMax Official Speech**, enter your mainland China API key, and save `speech-2.8-hd` or `speech-2.8-turbo`. The preset uses `https://api.minimax.cn/v1`. Unsupported model IDs are rejected when saving the service; choose a supported model from the preset. In an audio node, select that model, choose a system voice from **Voice library**, and enter up to 9,999 characters. Reference assets, voice cloning, voice design, and deletion are not supported by this integration. The system voice API does not provide previews; selecting a voice does not start a paid synthesis request. Compatible MiniMax speech models on the same account connection retain the selection. MiniMax speech uses the [synchronous T2A API](https://platform.minimax.cn/docs/api-reference/speech-t2a-http) and saves an MP3 in the project. A saved result URL permits download recovery without another synthesis request, even after removing the model configuration. The provider's URLs expire after 24 hours; an expired link cannot be recovered locally. An unknown submission is not retried automatically. This integration covers the mainland China endpoint; international endpoints and real-account availability need separate verification. diff --git a/docs/guides/user-guide.zh-CN.md b/docs/guides/user-guide.zh-CN.md index 52cd6c8b..b662426e 100644 --- a/docs/guides/user-guide.zh-CN.md +++ b/docs/guides/user-guide.zh-CN.md @@ -91,7 +91,7 @@ HTTP 400。 在 MOSS 开放平台上,可选择稳定版 `moss-tts-1.5-flash` 或 `moss-tts-1.0-pro`,也可以选择带日期的快照来固定版本。同一账户连接下,这四个型号共用音色库。每页最多显示 150 个 MOSS 音色,点击**下一页**或**上一页**可以翻页。筛选和收藏标签显示当前页中符合条件的音色。 -使用 MiniMax 时,选择**设置 → 模型接入 → 音频 → 添加服务 → MiniMax 官方语音**,填写中国站 API Key,保存 `speech-2.8-hd` 或 `speech-2.8-turbo`。预设地址为 `https://api.minimax.cn/v1`。在音频节点中选择该模型,从**音色库**选择系统音色,再填写最多 9,999 个字符的朗读文本。本接入不支持参考素材、音色克隆、音色设计或删除。系统音色接口不提供试听文件;选择音色不会发起付费合成请求。同一账户连接下的兼容 MiniMax 语音模型可以保留选择。 +使用 MiniMax 时,选择**设置 → 模型接入 → 音频 → 添加服务 → MiniMax 官方语音**,填写中国站 API Key,保存 `speech-2.8-hd` 或 `speech-2.8-turbo`。预设地址为 `https://api.minimax.cn/v1`。保存服务时会拒绝不支持的型号,请选择预设中的受支持型号。在音频节点中选择该模型,从**音色库**选择系统音色,再填写最多 9,999 个字符的朗读文本。本接入不支持参考素材、音色克隆、音色设计或删除。系统音色接口不提供试听文件;选择音色不会发起付费合成请求。同一账户连接下的兼容 MiniMax 语音模型可以保留选择。 MiniMax 配音使用[同步语音合成接口](https://platform.minimax.cn/docs/api-reference/speech-t2a-http),将 MP3 保存到工程。已有结果链接可用于恢复下载,不会重新提交合成;移除模型配置后仍可恢复。供应商链接有效期为 24 小时,过期后无法在本地恢复。提交结果不明时不会自动重试。本接入覆盖中国站地址,国际站和真实账户可用性需要单独验证。 diff --git a/packages/core/src/opendirector/application/creation_generation.py b/packages/core/src/opendirector/application/creation_generation.py index 6203ffc1..ec609903 100644 --- a/packages/core/src/opendirector/application/creation_generation.py +++ b/packages/core/src/opendirector/application/creation_generation.py @@ -602,10 +602,8 @@ async def recover(self, project_id, job_id, session=None, *, explicit=False): valid = False if not valid: if mode == 'download': - # The private locator belongs to the accepted result, not - # the current model selection. Never attach another model's - # credentials or turn this read into a new submission. - config = None + # Keep the accepted adapter and download policy without current credentials. + config = {**job.inputSnapshot.model.model_dump(mode='json'), 'apiKey': '', 'authMode': 'none'} else: task.update(status='interrupted', recovery='review', resumeAllowed=False) await asyncio.to_thread(self.checkpoint, project_id, task, pinned) diff --git a/packages/core/src/opendirector/domain/creation/commands.py b/packages/core/src/opendirector/domain/creation/commands.py index 96d7f1bf..d0e6d3e2 100644 --- a/packages/core/src/opendirector/domain/creation/commands.py +++ b/packages/core/src/opendirector/domain/creation/commands.py @@ -402,7 +402,7 @@ def command_fingerprint(command: Command) -> str: 'moss_speech_text', 'moss_speech_voice', 'moss_speech_reference', 'moss_prompt_limit', 'moss_image_size', 'moss_image_reference', 'moss_layout_reference', 'minimax_image_reference', 'minimax_image_count', 'minimax_image_bytes', - 'minimax_speech_text', 'minimax_speech_voice', 'minimax_speech_reference', + 'minimax_speech_text', 'minimax_speech_voice', 'minimax_speech_reference', 'minimax_speech_model', 'voice_library_unsupported', 'voice_connection_changed', 'voice_reference_required', 'voice_transcript_required', 'voice_transcript_too_long', 'voice_description_too_long', 'voice_not_found', 'voice_delete_unsupported', diff --git a/packages/core/src/opendirector/domain/input_rules.py b/packages/core/src/opendirector/domain/input_rules.py index 2a75e0d3..8468b4d1 100644 --- a/packages/core/src/opendirector/domain/input_rules.py +++ b/packages/core/src/opendirector/domain/input_rules.py @@ -5,7 +5,8 @@ from opendirector.domain.video_capabilities import capability_fields, seedance_20_variant from .moss_image import MOSS_IMAGE_PROMPT_CHARACTERS, is_moss_image from .moss_speech import is_moss_speech -from .minimax_speech import MINIMAX_SPEECH_TEXT_CHARACTERS, is_minimax_speech +from .minimax_speech import MINIMAX_SPEECH_MODELS, MINIMAX_SPEECH_TEXT_CHARACTERS, is_minimax_speech +from .projects import ProjectError from .kling_models import KLING_MODELS, KLING_RATIOS, kling_capabilities VideoMode = Literal['first_frame', 'reference', 'continuation'] @@ -34,6 +35,8 @@ def _adapter_rules(config): voiceRequired=True, referencesAllowed=False) return rules if is_minimax_speech(config): + if config['modelId'] not in MINIMAX_SPEECH_MODELS: + raise ProjectError('不支持该 MiniMax 语音型号,请选择预设中的型号。', 422, code='minimax_speech_model') rules.update(profile='minimax-speech', allowedKinds=[], maxImages=0, voiceRequired=True, referencesAllowed=False, maxPromptCharacters=MINIMAX_SPEECH_TEXT_CHARACTERS) return rules diff --git a/packages/core/src/opendirector/domain/minimax_speech.py b/packages/core/src/opendirector/domain/minimax_speech.py index 8ad99d74..48a90b6f 100644 --- a/packages/core/src/opendirector/domain/minimax_speech.py +++ b/packages/core/src/opendirector/domain/minimax_speech.py @@ -17,7 +17,7 @@ def is_minimax_speech(config): return (config.get('kind') == 'audio' and config.get('protocol') == 'openai' and config.get('connectionType', 'api') == 'api' and url.scheme == 'https' and url.hostname == 'api.minimax.cn' and url.port in {None, 443} - and url.path.rstrip('/') == '/v1' and config.get('modelId') in MINIMAX_SPEECH_MODELS) + and url.path.rstrip('/') == '/v1') def validate_minimax_speech_input(text, voice): diff --git a/src/contracts/creation.schema.json b/src/contracts/creation.schema.json index 37bddee1..2f716b05 100644 --- a/src/contracts/creation.schema.json +++ b/src/contracts/creation.schema.json @@ -3490,6 +3490,7 @@ "minimax_speech_text", "minimax_speech_voice", "minimax_speech_reference", + "minimax_speech_model", "voice_library_unsupported", "voice_connection_changed", "voice_reference_required", @@ -18660,6 +18661,7 @@ "minimax_speech_text", "minimax_speech_voice", "minimax_speech_reference", + "minimax_speech_model", "voice_library_unsupported", "voice_connection_changed", "voice_reference_required", diff --git a/src/contracts/generated.ts b/src/contracts/generated.ts index 58ad015f..fb740098 100644 --- a/src/contracts/generated.ts +++ b/src/contracts/generated.ts @@ -387,7 +387,7 @@ export interface ContractInput { } export interface ContractIssueInput extends ContractInput { - readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "minimax_speech_text" | "minimax_speech_voice" | "minimax_speech_reference" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; + readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "minimax_speech_text" | "minimax_speech_voice" | "minimax_speech_reference" | "minimax_speech_model" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; readonly message: string; readonly objectId?: string | null; readonly field?: string | null; @@ -1959,7 +1959,7 @@ export interface Contract { } export interface ContractIssue extends Contract { - readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "minimax_speech_text" | "minimax_speech_voice" | "minimax_speech_reference" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; + readonly code: "timeline_package_invalid" | "timeline_package_too_large" | "timeline_revision_missing" | "timeline_import_missing" | "timeline_import_conflict" | "timeline_registration_rejected" | "timeline_font_too_large" | "timeline_font_unavailable" | "timeline_font_unsupported" | "timeline_font_invalid" | "timeline_font_fingerprint" | "timeline_font_coverage" | "timeline_font_total_bytes" | "timeline_font_unverified" | "timeline_font_mime" | "timeline_bundled_font_fingerprint" | "timeline_bundled_font_unavailable" | "invalid_input" | "revision_conflict" | "role_restricted" | "missing_object" | "reference_invalid" | "kling_model" | "kling_mode" | "kling_image" | "kling_frames" | "kling_prompt" | "kling_aspect" | "kling_output" | "kling_cancel" | "capability_unsupported" | "preview_expired" | "storage_unavailable" | "submission_unknown" | "provider_error" | "moss_speech_text" | "moss_speech_voice" | "moss_speech_reference" | "moss_prompt_limit" | "moss_image_size" | "moss_image_reference" | "moss_layout_reference" | "minimax_image_reference" | "minimax_image_count" | "minimax_image_bytes" | "minimax_speech_text" | "minimax_speech_voice" | "minimax_speech_reference" | "minimax_speech_model" | "voice_library_unsupported" | "voice_connection_changed" | "voice_reference_required" | "voice_transcript_required" | "voice_transcript_too_long" | "voice_description_too_long" | "voice_not_found" | "voice_delete_unsupported" | "voice_consent_required" | "voice_name_invalid" | "voice_audio_invalid" | "voice_format" | "voice_size" | "voice_duration" | "voice_provider_protocol" | "voice_provider_error" | "voice_provider_timeout" | "voice_model_unsupported"; readonly message: string; readonly objectId: string | null; readonly field: string | null; diff --git a/tests/browser/voice_library.mjs b/tests/browser/voice_library.mjs index 2102fd45..a3c0c534 100644 --- a/tests/browser/voice_library.mjs +++ b/tests/browser/voice_library.mjs @@ -40,6 +40,12 @@ async function until(check) { throw Error("Expected persisted voice state was not observed"); } async function reopenAudio(page, assetPath, savedLabel) { + await waitForSaved(page, savedLabel); + // Acknowledge this page's lease release before reopening its persisted audio. + const released = page.waitForResponse(response => response.request().method() === "DELETE" && + new URL(response.url()).pathname.startsWith("/api/creation/sessions/")); + await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent("pagehide"))); + assert.ok((await released).ok()); const started = Date.now(); const statuses = []; let receive, timer; @@ -144,6 +150,7 @@ const copy = { settings: "Settings", models: "Model connections", audioOverview: "Audio model", addService: "Add service", provider: "Provider", speech: "MiniMax Official Speech", url: "Service URL (Base URL)", saveService: "Save service", manage: "Manage", + addModel: "Add manually", unsupportedModel: "Unsupported MiniMax speech model. Choose a model from the preset.", cancel: "Cancel", closeSettings: "Close settings", tools: "Canvas creation tools", add: "Add Audio", @@ -182,6 +189,7 @@ const copy = { settings: "设置", models: "模型接入", audioOverview: "音频模型", addService: "添加服务", provider: "提供商", speech: "MiniMax 官方语音", url: "服务地址(Base URL)", saveService: "保存服务", manage: "管理", + addModel: "手动添加", unsupportedModel: "不支持该 MiniMax 语音型号,请选择预设中的型号。", cancel: "取消", closeSettings: "关闭设置", tools: "画布创作工具", add: "添加音频", @@ -955,6 +963,13 @@ try { } assert.equal(await page.locator('.cw-model-option img[title="MiniMax"]').count(), 2); await page.screenshot({ path: `${output}/${locale}-minimax-provider.png`, fullPage: true }); + await page.getByRole("button", { name: L.addModel, exact: true }).click(); + await page.locator("#manual-model-id").fill("speech-2.8-unknown"); + await page.locator("#manual-model-id").press("Enter"); + await page.getByRole("button", { name: L.saveService, exact: true }).click(); + await page.getByRole("alert").filter({ hasText: L.unsupportedModel }).waitFor(); + await page.screenshot({ path: `${output}/${locale}-minimax-unsupported.png`, fullPage: true }); + await page.getByRole("checkbox", { name: "speech-2.8-unknown", exact: true }).uncheck(); await page.getByRole("button", { name: L.saveService, exact: true }).click(); await page.getByRole("button", { name: L.addService, exact: true }).waitFor(); const minimax = (await call("/api/model-configs")).filter(model => model.inputRules.profile === "minimax-speech"); diff --git a/tests/test_minimax_speech.py b/tests/test_minimax_speech.py index 513da1d6..437b6864 100644 --- a/tests/test_minimax_speech.py +++ b/tests/test_minimax_speech.py @@ -1,6 +1,7 @@ """MiniMax T2A wire contract and real MP3 persistence with mocked provider HTTP.""" import asyncio import json +import socket from uuid import uuid4 import httpx @@ -82,7 +83,7 @@ def test_official_models_have_read_only_voice_library_and_shared_rules(model): @pytest.mark.parametrize('patch', [ {'url': 'https://api.minimax.cn.evil.test/v1'}, {'url': 'https://api.minimax.io/v1'}, {'url': 'http://api.minimax.cn/v1'}, {'url': 'https://api.minimax.cn:8443/v1'}, - {'url': 'https://api.minimax.cn/v2'}, {'modelId': 'speech-2.8-unknown'}, + {'url': 'https://api.minimax.cn/v2'}, {'connectionType': 'self_hosted'}, ]) def test_other_services_and_lookalikes_keep_the_generic_adapter(patch): @@ -91,6 +92,33 @@ def test_other_services_and_lookalikes_keep_the_generic_adapter(patch): assert input_rules(config)['profile'] != 'minimax-speech' +@pytest.mark.parametrize('entry', [input_rules, adapter_for]) +def test_unknown_official_model_is_rejected(entry): + with pytest.raises(ProjectError) as failure: + entry({**CONFIG, 'modelId': 'speech-2.8-unknown'}) + assert failure.value.code == 'minimax_speech_model' + + +@pytest.mark.parametrize('batch', [False, True]) +@pytest.mark.parametrize('locale, message', [ + ('zh-CN', '不支持该 MiniMax 语音型号,请选择预设中的型号。'), + ('en-US', 'Unsupported MiniMax speech model. Choose a model from the preset.'), +]) +def test_unknown_model_save_is_localized_and_atomic(context, batch, locale, message): + project, configs, _ = context + before = configs.path.read_bytes() + payload = {**CONFIG, 'modelId': 'speech-2.8-unknown'} + if batch: + payload['modelIds'] = ['speech-2.8-hd', payload.pop('modelId')] + with TestClient(create_app(project[0].local), headers={ + 'X-OpenDirector-Request': '1', 'Accept-Language': locale, + }) as client: + response = client.post('/api/model-configs' + ('/batch' if batch else ''), json=payload) + assert response.status_code == 422 + assert response.json()['detail'] == message + assert configs.path.read_bytes() == before + + def test_system_voices_preserve_ids_and_account_scope(context): project, configs, saved = context calls = [] @@ -322,7 +350,8 @@ def handle(request): @pytest.mark.anyio -async def test_failed_download_recovers_after_model_removal_without_resubmitting(context, mp3): +@pytest.mark.parametrize('configuration_change', ['deleted', 'changed']) +async def test_failed_download_recovers_after_model_removal_without_resubmitting(context, mp3, configuration_change): project, configs, saved = context store, pid, session = project calls, healthy = [], False @@ -342,7 +371,10 @@ def handle(request): assert not store.snapshot(pid).assets finally: await first.close() - configs.delete(saved['id']) + if configuration_change == 'deleted': + configs.delete(saved['id']) + else: + configs.save('audio', 'https://changed.test/v1', 'tts-1', 'different-key', saved['id'], protocol='openai') healthy = True reopened = CreationProjects(LocalProjects(store.local.repository)) restarted = runner_for(((reopened, pid, session), configs, saved), handle) @@ -357,6 +389,57 @@ def handle(request): await restarted.close() +@pytest.mark.anyio +@pytest.mark.parametrize('configuration_change', ['deleted', 'changed']) +@pytest.mark.parametrize('destination', ['private', 'dns', 'redirect']) +async def test_recovery_keeps_public_download_checks(context, mp3, monkeypatch, configuration_change, destination): + project, configs, saved = context + store, pid, _ = project + calls, recovering = [], False + url = 'http://127.0.0.1/private' if destination == 'private' else 'https://cdn.test/speech.mp3' + monkeypatch.setattr(socket, 'getaddrinfo', lambda host, port, **kwargs: [ + (socket.AF_INET, socket.SOCK_STREAM, 6, '', + ('127.0.0.1' if recovering and destination == 'dns' else '93.184.215.14', port)), + ]) + def handle(request): + calls.append(request) + if request.url.path == '/v1/get_voice': return httpx.Response(200, json=VOICES) + if request.method == 'POST': + return httpx.Response(200, json={**SUCCESS, 'data': {'audio': url, 'status': 2}}) + if not recovering: raise httpx.ReadTimeout('download outage', request=request) + if destination == 'redirect' and request.url.path != '/private': + return httpx.Response(302, headers={'Location': 'http://127.0.0.1/private'}) + return httpx.Response(200, content=mp3) + first = runner_for(context, handle) + try: + first._launch(*prepared(first, project, saved, prompt='Read this', parameters=selection(saved))) + await settled(first) + [job] = store.snapshot(pid).jobs + assert job.recovery == 'download' + finally: + await first.close() + if configuration_change == 'deleted': + configs.delete(saved['id']) + else: + configs.save('audio', 'https://changed.test/v1', 'tts-1', 'different-key', saved['id'], protocol='openai') + recovering = True + before = len(calls) + restarted = runner_for(context, handle) + try: + await restarted.start() + await settled(restarted) + snapshot = store.snapshot(pid) + assert snapshot.jobs[0].errorDetails.category == 'protocol' + assert snapshot.jobs[0].errorDetails.stage == 'download' + assert len(calls) - before == (1 if destination == 'redirect' else 0) + assert all(call.url.host == '93.184.215.14' and 'authorization' not in call.headers + for call in calls if call.method == 'GET') + assert not snapshot.assets and not snapshot.results + assert sum(call.url.path == '/v1/t2a_v2' for call in calls) == 1 + finally: + await restarted.close() + + @pytest.mark.anyio async def test_invalid_mp3_never_publishes_an_asset(context): project, _, saved = context