diff --git a/app/services/studio_music_spec.py b/app/services/studio_music_spec.py index 0865e534..cc908748 100644 --- a/app/services/studio_music_spec.py +++ b/app/services/studio_music_spec.py @@ -343,7 +343,8 @@ def _check_lora_names(cls, values): @model_validator(mode="after") def _check_semantics(self): _non_blank(self.prompt, "input.params.prompt") - _non_blank(self.alt_prompt, "input.params.alt_prompt") + # Caption/style is optional. ACE-Step accepts an empty alt_prompt; + # MiniMax-Music3 still rejects a blank caption in its handler. _non_blank(self.model_type, "input.params.model_type") if self.model_type not in STUDIO_MUSIC_MODEL_TYPES: raise ValueError("input.params.model_type is not a registered local music model") diff --git a/tests/test_studio_music_spec.py b/tests/test_studio_music_spec.py index bbf7ccc1..3c1d06c9 100644 --- a/tests/test_studio_music_spec.py +++ b/tests/test_studio_music_spec.py @@ -103,6 +103,19 @@ def test_instrumental_marker_remains_literal_and_is_not_rewritten(): assert frozen["effective"]["input"]["params"]["alt_prompt"] == command["input"]["params"]["alt_prompt"] +def test_empty_or_omitted_alt_prompt_is_admitted(): + blank = music_command() + blank["input"]["params"]["alt_prompt"] = "" + frozen_blank = freeze_studio_music_spec(blank) + assert frozen_blank["effective"]["input"]["params"]["alt_prompt"] == "" + + omitted = music_command() + omitted["input"]["params"].pop("alt_prompt") + frozen_omitted = freeze_studio_music_spec(omitted) + assert "alt_prompt" not in frozen_omitted["original"]["input"]["params"] + assert frozen_omitted["effective"]["input"]["params"].get("alt_prompt", "") == "" + + def test_fingerprint_excludes_intent_but_covers_workspace_collection_and_content(): first = freeze_studio_music_spec(music_command("first")) second = freeze_studio_music_spec(music_command("second")) diff --git a/ui/src/features/studio/musicGenerationSpec.ts b/ui/src/features/studio/musicGenerationSpec.ts index a8f5dee2..0c70164c 100644 --- a/ui/src/features/studio/musicGenerationSpec.ts +++ b/ui/src/features/studio/musicGenerationSpec.ts @@ -341,7 +341,9 @@ function assertMusicParams(value: unknown): asserts value is StudioMusicParams { } assertCatalogValue(value, paramsSchema, 'input.params') requiredText(value.prompt, 'input.params.prompt', MAX_PROMPT_LENGTH) - requiredText(value.alt_prompt, 'input.params.alt_prompt', MAX_PROMPT_LENGTH) + // alt_prompt is optional in the generated catalog (default ""). Requiring a + // non-blank caption blocked Studio Generate and Wizard start_generation + // when the user wrote lyrics or [Instrumental] without filling Style. requiredText(value.model_type, 'input.params.model_type', MAX_WORKSPACE_LENGTH) assertMusicSelectors(value) assertAudioReferences(value) diff --git a/ui/tests/musicGenerationCommands.test.ts b/ui/tests/musicGenerationCommands.test.ts index 843c0eb5..475cc355 100644 --- a/ui/tests/musicGenerationCommands.test.ts +++ b/ui/tests/musicGenerationCommands.test.ts @@ -196,6 +196,20 @@ test('music builder retains inactive sentinels but rejects active modes, TTS and ) }) +test('music builder admits empty or omitted style captions', { concurrency: false }, () => { + const blank = command('music-blank-caption', { alt_prompt: '' }) + assert.equal(blank.input.params.alt_prompt, '') + assert.equal(blank.input.params.prompt, nativeParams().prompt) + + const { alt_prompt: _omitted, ...withoutCaption } = nativeParams() + const omitted = createStudioMusicGenerationCommand({ + ...withoutCaption, + workspace: 'music-workspace', + }, 'music-omitted-caption') + assert.equal('alt_prompt' in omitted.input.params, false) + assert.equal(omitted.input.params.prompt, withoutCaption.prompt) +}) + test('form projection preserves music text, language, refs and sentinels while rejecting active stale controls', { concurrency: false }, () => { const source = { ...nativeParams(),