diff --git a/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs b/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs index e9fa9e674..a6b410774 100644 --- a/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs +++ b/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs @@ -443,7 +443,7 @@ private static void Advance(Runner r) // RCE. Anything outside these sets is rejected. Mirrors ModelImportPipeline's allowlist style. private static readonly HashSet AudioAllowedExtensions = new(StringComparer.OrdinalIgnoreCase) { - "wav", "mp3", "ogg", "aiff", "aif", "flac", + "wav", "mp3", "pcm", "ogg", "aiff", "aif", "flac", }; private static readonly HashSet ImageAllowedExtensions = new(StringComparer.OrdinalIgnoreCase) { diff --git a/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs b/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs index ef25af0da..36f5e3220 100644 --- a/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs +++ b/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs @@ -68,6 +68,10 @@ public static class AssetGenModelCatalog new ModelEntry { Id = "cassetteai/music-generator", Label = "CassetteAI Music", Provider = "fal", Kind = "audio", UseCase = "Background music", PriceLabel = "$0.02/min", MaxDurationSeconds = 180f, DurationField = "duration", DefaultDurationSeconds = 10f, MinDurationSeconds = 1f }, new ModelEntry { Id = "fal-ai/lyria2", Label = "Google Lyria 2", Provider = "fal", Kind = "audio", UseCase = "Background music", PriceLabel = "$0.10/30s", MaxDurationSeconds = 30f }, + + // Audio cover — MiniMax. Reference input must be 6-360 seconds and no larger than 50 MB. + new ModelEntry { Id = MiniMaxAudioAdapter.DefaultModel, Label = "MiniMax Music Cover", Provider = "minimax", Kind = "audio", UseCase = "Reference-audio cover" }, + new ModelEntry { Id = "music-cover-free", Label = "MiniMax Music Cover (free)", Provider = "minimax", Kind = "audio", UseCase = "Reference-audio cover" }, }; /// Curated entries for a provider+kind, in curated order (default first). Never null. diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs b/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs index 026de16b3..bc8a8b787 100644 --- a/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs +++ b/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs @@ -44,6 +44,8 @@ public static IAudioProviderAdapter Audio(string id) { case "fal": return new FalAudioAdapter(); + case "minimax": + return new MiniMaxAudioAdapter(); default: throw new NotSupportedException($"Unknown audio provider '{id}'."); } @@ -71,6 +73,7 @@ public static IReadOnlyList List() new ProviderInfo { Id = "openrouter", Kind = "image", Configured = IsConfigured("openrouter"), Capabilities = new[] { "text", "image" } }, // fal appears twice by design — once per kind (image + audio) — sharing the single "fal" key. new ProviderInfo { Id = "fal", Kind = "audio", Configured = IsConfigured("fal"), Capabilities = new[] { "text", "music", "sfx" } }, + new ProviderInfo { Id = "minimax", Kind = "audio", Configured = IsConfigured("minimax"), Capabilities = new[] { "music", "cover", "url", "base64" } }, }; } diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs new file mode 100644 index 000000000..4c132c5f1 --- /dev/null +++ b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs @@ -0,0 +1,247 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MCPForUnity.Editor.Security; +using MCPForUnity.Editor.Services.AssetGen.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Services.AssetGen.Providers +{ + /// + /// MiniMax music-cover adapter for the synchronous music-generation endpoint. A cover accepts + /// one raw reference source (URL or base64 audio) and an optional preprocessed feature id, then + /// exposes the returned URL or hex payload through the normal audio job pipeline. + /// + public sealed class MiniMaxAudioAdapter : IAudioProviderAdapter + { + internal const string GlobalEndpoint = "https://api.minimax.io/v1/music_generation"; + internal const string ChinaEndpoint = "https://api.minimaxi.com/v1/music_generation"; + internal const string RegionEnvVar = "MCPFORUNITY_MINIMAX_REGION"; + internal const string DefaultModel = "music-cover"; + + private const string GlobalHost = "api.minimax.io"; + private const string ChinaHost = "api.minimaxi.com"; + private const string FreeModel = "music-cover-free"; + + private byte[] _inlineData; + private string _downloadUrl; + private string _resultExt; + private string _error; + + public string Id => "minimax"; + + internal static bool IsCoverModel(string model) + => string.Equals(model, DefaultModel, StringComparison.Ordinal) + || string.Equals(model, FreeModel, StringComparison.Ordinal); + + public async Task SubmitAsync( + AudioGenRequest req, + string apiKey, + IHttpTransport http, + CancellationToken ct) + { + if (req == null) throw new ArgumentNullException(nameof(req)); + if (http == null) throw new ArgumentNullException(nameof(http)); + + string model = string.IsNullOrWhiteSpace(req.Model) ? DefaultModel : req.Model; + ValidateRequest(req, model); + ResolveRegion(out string endpoint, out string host, out bool chinaRegion); + ProviderHttp.RequireHost(endpoint, host, apiKey, "MiniMax cover submit"); + + var spec = new HttpRequestSpec + { + Method = "POST", + Url = endpoint, + ContentType = "application/json", + Body = Encoding.UTF8.GetBytes(BuildBody(req, model, chinaRegion).ToString(Formatting.None)) + }; + spec.Headers["Authorization"] = "Bearer " + apiKey; + + HttpResult response = await http.SendAsync(spec, ct); + JObject json = ParseOk(response, apiKey); + + int statusCode = AsInt(json["base_resp"]?["status_code"], -1); + if (statusCode != 0) + { + string statusMessage = json["base_resp"]?["status_msg"]?.ToString(); + _error = SecretRedactor.Scrub( + $"MiniMax music cover failed (status_code={statusCode}): {statusMessage ?? "unknown error"}", + apiKey); + return "ready"; + } + + JToken data = json["data"]; + int generationStatus = AsInt(data?["status"], -1); + if (generationStatus != 2) + { + _error = generationStatus == 1 + ? "MiniMax music cover is still in progress, but the response included no query endpoint." + : $"MiniMax music cover returned an unexpected status ({generationStatus})."; + return "ready"; + } + + string audio = data?["audio"]?.ToString(); + if (string.IsNullOrWhiteSpace(audio)) + { + _error = "MiniMax music cover completed without audio data."; + return "ready"; + } + + if (Uri.TryCreate(audio, UriKind.Absolute, out Uri audioUri) + && (audioUri.Scheme == Uri.UriSchemeHttp || audioUri.Scheme == Uri.UriSchemeHttps)) + { + _downloadUrl = audio; + } + else + { + _inlineData = TryDecodeHex(audio); + if (_inlineData == null || _inlineData.Length == 0) + { + _error = "MiniMax returned an unrecognized audio payload."; + return "ready"; + } + } + + _resultExt = NormalizeAudioFormat(req.AudioFormat); + return "ready"; + } + + public Task PollAsync( + string providerJobId, + string apiKey, + IHttpTransport http, + CancellationToken ct) + { + var result = new ProviderPollResult { Progress = 1f }; + if (!string.IsNullOrEmpty(_error) || (_inlineData == null && string.IsNullOrEmpty(_downloadUrl))) + { + result.State = ProviderPollState.Failed; + result.Error = _error ?? "MiniMax produced no cover audio."; + } + else + { + result.State = ProviderPollState.Succeeded; + result.InlineData = _inlineData; + result.DownloadUrl = _downloadUrl; + result.ResultExt = _resultExt; + } + return Task.FromResult(result); + } + + private static void ValidateRequest(AudioGenRequest req, string model) + { + if (!IsCoverModel(model)) + throw new NotSupportedException("MiniMax audio supports music-cover and music-cover-free."); + + int sourceCount = 0; + if (!string.IsNullOrWhiteSpace(req.AudioUrl)) sourceCount++; + if (!string.IsNullOrWhiteSpace(req.AudioBase64)) sourceCount++; + if (sourceCount != 1) + throw new ArgumentException("Provide exactly one cover source: audio_url or audio_base64."); + + NormalizeOutputFormat(req.OutputFormat); + NormalizeAudioFormat(req.AudioFormat); + } + + private static JObject BuildBody(AudioGenRequest req, string model, bool chinaRegion) + { + var body = new JObject + { + ["model"] = model, + ["stream"] = false, + ["output_format"] = NormalizeOutputFormat(req.OutputFormat), + ["audio_setting"] = new JObject + { + ["format"] = NormalizeAudioFormat(req.AudioFormat) + } + }; + + if (!string.IsNullOrWhiteSpace(req.Prompt)) body["prompt"] = req.Prompt; + if (!string.IsNullOrWhiteSpace(req.Lyrics)) body["lyrics"] = req.Lyrics; + if (req.LyricsOptimizer.HasValue) body["lyrics_optimizer"] = req.LyricsOptimizer.Value; + if (req.IsInstrumental.HasValue) body["is_instrumental"] = req.IsInstrumental.Value; + if (!string.IsNullOrWhiteSpace(req.AudioUrl)) body["audio_url"] = req.AudioUrl; + if (!string.IsNullOrWhiteSpace(req.AudioBase64)) body["audio_base64"] = req.AudioBase64; + if (!string.IsNullOrWhiteSpace(req.CoverFeatureId)) body["cover_feature_id"] = req.CoverFeatureId; + if (chinaRegion && req.AigcWatermark.HasValue) body["aigc_watermark"] = req.AigcWatermark.Value; + return body; + } + + private static string NormalizeOutputFormat(string value) + { + string normalized = string.IsNullOrWhiteSpace(value) ? "url" : value.Trim().ToLowerInvariant(); + if (normalized != "url" && normalized != "hex") + throw new ArgumentException("output_format must be 'url' or 'hex'."); + return normalized; + } + + private static string NormalizeAudioFormat(string value) + { + string normalized = string.IsNullOrWhiteSpace(value) ? "mp3" : value.Trim().ToLowerInvariant(); + if (normalized != "mp3" && normalized != "wav" && normalized != "pcm") + throw new ArgumentException("audio_format must be 'mp3', 'wav', or 'pcm'."); + return normalized; + } + + private static void ResolveRegion(out string endpoint, out string host, out bool chinaRegion) + { + string region = (Environment.GetEnvironmentVariable(RegionEnvVar) ?? string.Empty) + .Trim().ToLowerInvariant(); + chinaRegion = region == "cn" || region == "cn_zh" || region == "china"; + endpoint = chinaRegion ? ChinaEndpoint : GlobalEndpoint; + host = chinaRegion ? ChinaHost : GlobalHost; + } + + private static int AsInt(JToken token, int fallback) + { + if (token == null || token.Type == JTokenType.Null) return fallback; + if (token.Type == JTokenType.Integer) return (int)token; + return int.TryParse(token.ToString(), out int parsed) ? parsed : fallback; + } + + private static byte[] TryDecodeHex(string hex) + { + if (string.IsNullOrEmpty(hex) || (hex.Length % 2) != 0) return null; + var bytes = new byte[hex.Length / 2]; + for (int i = 0; i < bytes.Length; i++) + { + int high = HexValue(hex[i * 2]); + int low = HexValue(hex[i * 2 + 1]); + if (high < 0 || low < 0) return null; + bytes[i] = (byte)((high << 4) | low); + } + return bytes; + } + + private static int HexValue(char value) + { + if (value >= '0' && value <= '9') return value - '0'; + if (value >= 'a' && value <= 'f') return value - 'a' + 10; + if (value >= 'A' && value <= 'F') return value - 'A' + 10; + return -1; + } + + private static JObject ParseOk(HttpResult response, string apiKey) + { + string text = ProviderHttp.BodyText(response); + JObject json = null; + if (!string.IsNullOrEmpty(text)) + { + try { json = JObject.Parse(text); } + catch { /* handled below */ } + } + + if (response?.Ok != true) + { + string detail = json?["base_resp"]?["status_msg"]?.ToString() + ?? json?["error"]?.ToString() + ?? ProviderHttp.Truncate(text); + throw new Exception(SecretRedactor.Scrub( + $"MiniMax cover request failed (status={response?.Status}): {detail}", apiKey)); + } + return json ?? new JObject(); + } + } +} diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs.meta b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs.meta new file mode 100644 index 000000000..812b7458b --- /dev/null +++ b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: edcf0554d7a94b0ab2d469529e08c2d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/ProviderModels.cs b/MCPForUnity/Editor/Services/AssetGen/Providers/ProviderModels.cs index 1e8d0f9e1..d4c8e2613 100644 --- a/MCPForUnity/Editor/Services/AssetGen/Providers/ProviderModels.cs +++ b/MCPForUnity/Editor/Services/AssetGen/Providers/ProviderModels.cs @@ -63,16 +63,25 @@ public sealed class ImageGenRequest } /// - /// Request to generate an audio clip (fal.ai for v1). selects the fal - /// endpoint (stable-audio-25 / cassetteai/* / lyria2). is a per-gen - /// input; 0 => provider default. Never carries a key; never persisted (transient request only). + /// Request to generate an audio clip. Cover-capable providers can consume reference audio as + /// a URL or base64 payload plus optional cover metadata. Never carries a key and is never + /// persisted (transient request only). /// public sealed class AudioGenRequest { - public string Provider; // "fal" for v1 - public string Model; // fal model id, e.g. fal-ai/stable-audio-25/text-to-audio + public string Provider; + public string Model; public string Prompt; public float Duration; // seconds; 0 => per-model default. Soft-clamped per model in the adapter. + public string Lyrics; + public bool? LyricsOptimizer; + public bool? IsInstrumental; + public string AudioUrl; + public string AudioBase64; + public string CoverFeatureId; + public string OutputFormat; + public string AudioFormat; + public bool? AigcWatermark; public string Name; public string OutputFolder; } diff --git a/MCPForUnity/Editor/Tools/AssetGen/GenerateAudio.cs b/MCPForUnity/Editor/Tools/AssetGen/GenerateAudio.cs index 598d7c443..2963ebcf2 100644 --- a/MCPForUnity/Editor/Tools/AssetGen/GenerateAudio.cs +++ b/MCPForUnity/Editor/Tools/AssetGen/GenerateAudio.cs @@ -8,8 +8,8 @@ namespace MCPForUnity.Editor.Tools.AssetGen { /// - /// Audio generation (SFX / music) via fal.ai. Triggered here (never from the GUI); the C# side - /// reads the fal key from the secure store and runs the job. Returns a job_id immediately; the + /// Audio generation and cover creation. Triggered here (never from the GUI); the C# side reads + /// the provider key from the secure store and runs the job. Returns a job_id immediately; the /// client polls the `status` action. When `model` is omitted it falls back to the model selected /// in the Asset Generation tab, then the catalog default. Status / cancel / list_providers are /// shared across the generate_* tools via . @@ -53,21 +53,43 @@ private static object Generate(ToolParams p) if (!SecureKeyStore.Current.Has(provider)) return new ErrorResponse(AssetGenProviders.MissingKeyMessage(provider)); - string prompt = p.Get("prompt"); - if (string.IsNullOrWhiteSpace(prompt)) - return new ErrorResponse("'prompt' is required for audio generation."); - // Empty -> GUI-selected model -> catalog default. A null model reaches the adapter's own // default; a resolved id is passed through verbatim (the catalog default equals the // adapter constant, so an omitted model is a no-op either way). string model = AssetGenModelCatalog.ResolveModel("audio", provider, p.Get("model")); + string prompt = p.Get("prompt"); + if (string.Equals(provider, "minimax", StringComparison.OrdinalIgnoreCase)) + { + if (!MiniMaxAudioAdapter.IsCoverModel(model)) + return new ErrorResponse("The MiniMax audio provider supports music-cover and music-cover-free."); + + int sourceCount = 0; + if (!string.IsNullOrWhiteSpace(p.Get("audioUrl"))) sourceCount++; + if (!string.IsNullOrWhiteSpace(p.Get("audioBase64"))) sourceCount++; + if (sourceCount != 1) + return new ErrorResponse("Provide exactly one of 'audioUrl' or 'audioBase64'."); + } + else if (string.IsNullOrWhiteSpace(prompt)) + { + return new ErrorResponse("'prompt' is required for audio generation."); + } + var req = new AudioGenRequest { Provider = provider, Model = model, Prompt = prompt, Duration = p.GetFloat("duration", 0f) ?? 0f, + Lyrics = p.Get("lyrics"), + LyricsOptimizer = p.Has("lyricsOptimizer") ? p.GetBool("lyricsOptimizer") : null, + IsInstrumental = p.Has("isInstrumental") ? p.GetBool("isInstrumental") : null, + AudioUrl = p.Get("audioUrl"), + AudioBase64 = p.Get("audioBase64"), + CoverFeatureId = p.Get("coverFeatureId"), + OutputFormat = p.Get("outputFormat"), + AudioFormat = p.Get("audioFormat"), + AigcWatermark = p.Has("aigcWatermark") ? p.GetBool("aigcWatermark") : null, Name = p.Get("name"), OutputFolder = p.Get("outputFolder"), }; diff --git a/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs b/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs index 6b0e6d113..8052799e9 100644 --- a/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs +++ b/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs @@ -181,8 +181,9 @@ private void BuildProviderRows() AddProviderRow(imagePanel, provider.Id, provider.Label, "image"); } - var audioPanel = AddCategoryPanel("Sound (fal.ai)"); + var audioPanel = AddCategoryPanel("Sound"); AddAudioRow(audioPanel); + AddProviderRow(audioPanel, "minimax", "MiniMax", "audio"); AddBlenderHandoffRow(); } diff --git a/Server/src/cli/commands/asset_gen.py b/Server/src/cli/commands/asset_gen.py index 99b41d77b..697f78426 100644 --- a/Server/src/cli/commands/asset_gen.py +++ b/Server/src/cli/commands/asset_gen.py @@ -201,10 +201,19 @@ def generate_image( @asset_gen.command("generate-audio") -@click.option("--provider", default=None, help="Provider id (fal).") +@click.option("--provider", default=None, help="Provider id.") @click.option("--prompt", default=None, help="Text prompt describing the sound or music.") -@click.option("--model", default=None, help="fal model id (omit for the GUI-selected default).") +@click.option("--model", default=None, help="Provider model id (omit for the GUI-selected default).") @click.option("--duration", default=None, type=float, help="Requested length in seconds (soft-clamped per model).") +@click.option("--lyrics", default=None, help="Optional cover lyrics.") +@click.option("--lyrics-optimizer/--no-lyrics-optimizer", default=None, help="Set the lyrics optimization option.") +@click.option("--is-instrumental/--with-vocals", default=None, help="Set the instrumental cover option.") +@click.option("--audio-url", default=None, help="Reference-audio URL for a cover.") +@click.option("--audio-base64", default=None, help="Base64 reference audio for a cover.") +@click.option("--cover-feature-id", default=None, help="Preprocessed cover feature id.") +@click.option("--output-format", type=click.Choice(["url", "hex"]), default=None, help="Provider response format.") +@click.option("--audio-format", type=click.Choice(["mp3", "wav", "pcm"]), default=None, help="Generated audio encoding.") +@click.option("--aigc-watermark/--no-aigc-watermark", default=None, help="Set the regional AIGC watermark option.") @click.option("--name", default=None, help="Base name for the imported asset.") @click.option("--output-folder", default=None, help="Destination folder under Assets/.") @handle_unity_errors @@ -213,15 +222,23 @@ def generate_audio( prompt: Optional[str], model: Optional[str], duration: Optional[float], + lyrics: Optional[str], + lyrics_optimizer: Optional[bool], + is_instrumental: Optional[bool], + audio_url: Optional[str], + audio_base64: Optional[str], + cover_feature_id: Optional[str], + output_format: Optional[str], + audio_format: Optional[str], + aigc_watermark: Optional[bool], name: Optional[str], output_folder: Optional[str], ): - """Generate audio (SFX / music) with a fal.ai model. + """Generate or cover audio and import it into Unity. \b Examples: - unity-mcp asset-gen generate-audio --provider fal --prompt "8-bit coin pickup" - unity-mcp asset-gen generate-audio --prompt "calm ambient loop" --duration 30 + unity-mcp asset-gen generate-audio --provider minimax --model music-cover --prompt "Warm acoustic folk cover" --audio-url https://example.com/reference.mp3 """ config = get_config() @@ -231,6 +248,15 @@ def generate_audio( "prompt": prompt, "model": model, "duration": duration, + "lyrics": lyrics, + "lyricsOptimizer": lyrics_optimizer, + "isInstrumental": is_instrumental, + "audioUrl": audio_url, + "audioBase64": audio_base64, + "coverFeatureId": cover_feature_id, + "outputFormat": output_format, + "audioFormat": audio_format, + "aigcWatermark": aigc_watermark, "name": name, "outputFolder": output_folder, } diff --git a/Server/src/services/tools/generate_audio.py b/Server/src/services/tools/generate_audio.py index 04466f837..7f8de1ac2 100644 --- a/Server/src/services/tools/generate_audio.py +++ b/Server/src/services/tools/generate_audio.py @@ -1,9 +1,9 @@ """ -Defines the generate_audio tool for AI audio (SFX / music) generation in Unity. +Defines the generate_audio tool for AI audio generation in Unity. -Thin pass-through: this tool carries NO API keys and NO file bytes. The C# side -reads the user's fal.ai key from the OS secure store, performs the provider -HTTPS call, downloads the result, and imports it as an AudioClip. +Thin pass-through: this tool carries no API keys. The C# side reads provider +credentials from the OS secure store, performs the HTTPS call, downloads the +result, and imports it as an AudioClip. """ from typing import Annotated, Any, Literal @@ -19,17 +19,13 @@ @mcp_for_unity_tool( group="asset_gen", description=( - "Generate audio (sound effects and background music) with fal.ai models and import " - "them as AudioClips into the Unity project. Bring-your-own-key: the fal key lives in " - "the editor's secure store (shared with image generation) and never crosses the bridge.\n\n" - "MODELS (all via fal.ai): fal-ai/stable-audio-25/text-to-audio (music + SFX, <=190s), " - "cassetteai/sound-effects-generator (SFX, <=30s), cassetteai/music-generator (music), " - "fal-ai/lyria2 (music). Omit model to use the model selected in the " - "MCP for Unity -> Asset Generation tab.\n\n" + "Generate audio and import it as an AudioClip into the Unity project. Provider keys " + "live in the editor's secure store and never cross the bridge. Omit model to use the " + "model selected in the MCP for Unity -> Asset Generation tab.\n\n" "ACTIONS:\n" - "- generate: Submit an audio job from a text prompt. Returns { job_id }; poll with the " - "status action. Params: provider (fal), prompt, model, duration (seconds), name, " - "output_folder.\n" + "- generate: Submit an audio job. Cover models require one of audio_url or " + "audio_base64 and also accept cover_feature_id. Returns { job_id }; poll with the " + "status action. URL results expire after 24 hours.\n" "- status: Poll an async job by job_id -> { state, progress, assetPath?, error? }.\n" "- cancel: Cancel an in-flight job by job_id.\n" "- list_providers: List configured audio providers and capabilities (no key values)." @@ -44,11 +40,19 @@ async def generate_audio( action: Annotated[Literal["generate", "status", "cancel", "list_providers"], "Action to perform."], - provider: Annotated[str, "Provider id (fal)."] | None = None, + provider: Annotated[str, "Provider id."] | None = None, prompt: Annotated[str, "Text prompt describing the sound or music."] | None = None, - model: Annotated[str, "fal model id (e.g. fal-ai/stable-audio-25/text-to-audio). " - "Omit to use the GUI-selected default."] | None = None, + model: Annotated[str, "Provider model id. Omit to use the GUI-selected default."] | None = None, duration: Annotated[float, "Requested length in seconds (soft-clamped per model)."] | None = None, + lyrics: Annotated[str, "Optional lyrics for a cover."] | None = None, + lyrics_optimizer: Annotated[bool, "Whether to optimize the supplied lyrics."] | None = None, + is_instrumental: Annotated[bool, "Whether to generate an instrumental cover."] | None = None, + audio_url: Annotated[str, "Reference-audio URL for a cover (6-360 seconds, at most 50 MB)."] | None = None, + audio_base64: Annotated[str, "Base64 reference audio for a cover (6-360 seconds, at most 50 MB)."] | None = None, + cover_feature_id: Annotated[str, "Optional preprocessed cover feature id."] | None = None, + output_format: Annotated[Literal["url", "hex"], "Provider response format; URL results expire after 24 hours."] | None = None, + audio_format: Annotated[Literal["mp3", "wav", "pcm"], "Generated audio encoding."] | None = None, + aigc_watermark: Annotated[bool, "Whether to add the regional AIGC watermark."] | None = None, name: Annotated[str, "Base name for the imported asset."] | None = None, output_folder: Annotated[str, "Destination folder under Assets/ for the import."] | None = None, job_id: Annotated[str, "Job id for status/cancel."] | None = None, @@ -61,6 +65,15 @@ async def generate_audio( "prompt": prompt, "model": model, "duration": duration, + "lyrics": lyrics, + "lyricsOptimizer": lyrics_optimizer, + "isInstrumental": is_instrumental, + "audioUrl": audio_url, + "audioBase64": audio_base64, + "coverFeatureId": cover_feature_id, + "outputFormat": output_format, + "audioFormat": audio_format, + "aigcWatermark": aigc_watermark, "name": name, "outputFolder": output_folder, "jobId": job_id, diff --git a/Server/tests/test_asset_gen_audio.py b/Server/tests/test_asset_gen_audio.py index 462854f7c..c7b3ad551 100644 --- a/Server/tests/test_asset_gen_audio.py +++ b/Server/tests/test_asset_gen_audio.py @@ -1,6 +1,6 @@ -"""Tests for the generate_audio asset-gen tool and CLI command (fal.ai). +"""Tests for the generate_audio asset-gen tool and CLI command. -Pass-through tool: NO API keys, NO file bytes. Unity transport fully mocked. +Pass-through tool: no API keys. Unity transport fully mocked. """ import asyncio @@ -18,7 +18,9 @@ COMMAND = "generate_audio" ALLOWED_KEYS = { - "action", "provider", "prompt", "model", "duration", "name", "outputFolder", "jobId", + "action", "provider", "prompt", "model", "duration", "lyrics", "audioUrl", + "lyricsOptimizer", "isInstrumental", "audioBase64", "coverFeatureId", "outputFormat", + "audioFormat", "aigcWatermark", "name", "outputFolder", "jobId", } @@ -107,6 +109,42 @@ def test_explicit_model_passes_through(self): model="cassetteai/sound-effects-generator") assert _sent_params(sent)["model"] == "cassetteai/sound-effects-generator" + def test_cover_fields_map_to_bridge_names(self): + _, sent = _call_tool( + action="generate", + provider="minimax", + model="music-cover", + prompt="Warm acoustic folk cover", + audio_url="https://example.com/reference.mp3", + lyrics="[Verse]\nA rewritten verse", + lyrics_optimizer=True, + is_instrumental=False, + output_format="hex", + audio_format="wav", + aigc_watermark=True, + ) + params = _sent_params(sent) + assert params["audioUrl"] == "https://example.com/reference.mp3" + assert params["lyrics"] == "[Verse]\nA rewritten verse" + assert params["lyricsOptimizer"] is True + assert params["isInstrumental"] is False + assert params["outputFormat"] == "hex" + assert params["audioFormat"] == "wav" + assert params["aigcWatermark"] is True + for snake in ("audio_url", "audio_base64", "cover_feature_id", "output_format", "audio_format"): + assert snake not in params + + def test_cover_feature_id_passes_through(self): + _, sent = _call_tool( + action="generate", + provider="minimax", + model="music-cover-free", + audio_base64="SUQz", + cover_feature_id="feature-id", + ) + assert _sent_params(sent)["audioBase64"] == "SUQz" + assert _sent_params(sent)["coverFeatureId"] == "feature-id" + def test_no_secret_keys_in_payload(self): _, sent = _call_tool( action="generate", provider="fal", prompt="p", @@ -143,3 +181,16 @@ def test_generate_audio_cli(self, cli_runner): assert params["provider"] == "fal" assert params["duration"] == 30.0 assert set(params.keys()).issubset(ALLOWED_KEYS) + + def test_generate_audio_cover_cli(self, cli_runner): + result, mock_run = cli_runner([ + "generate-audio", "--provider", "minimax", "--model", "music-cover", + "--prompt", "Warm acoustic folk cover", "--audio-url", "https://example.com/reference.mp3", + "--output-format", "url", "--audio-format", "mp3", "--aigc-watermark", + ]) + assert result.exit_code == 0 + params = mock_run.call_args.args[1] + assert params["audioUrl"] == "https://example.com/reference.mp3" + assert params["outputFormat"] == "url" + assert params["audioFormat"] == "mp3" + assert params["aigcWatermark"] is True diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs index 9ef048e6c..694cc92fa 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs @@ -80,6 +80,17 @@ public void DefaultModelId_MatchesAdapterConstants() Assert.AreEqual(MeshyAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("meshy", "model")); Assert.AreEqual(FalAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("fal", "image")); Assert.AreEqual(FalAudioAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("fal", "audio")); + Assert.AreEqual(MiniMaxAudioAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("minimax", "audio")); + } + + [Test] + public void Curated_HasMiniMaxCoverModels() + { + IReadOnlyList audio = AssetGenModelCatalog.ForProvider("minimax", "audio"); + + CollectionAssert.AreEqual( + new[] { "music-cover", "music-cover-free" }, + audio.Select(e => e.Id).ToList()); } [Test] diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs index 2b8e0b84f..5174bdefc 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs @@ -35,6 +35,14 @@ public void Audio_Fal_ReturnsAdapter() Assert.AreEqual("fal", adapter.Id); } + [Test] + public void Audio_MiniMax_ReturnsAdapter() + { + IAudioProviderAdapter adapter = AssetGenProviders.Audio("minimax"); + Assert.IsNotNull(adapter); + Assert.AreEqual("minimax", adapter.Id); + } + [Test] public void Audio_Unimplemented_Throws() { @@ -49,6 +57,17 @@ public void List_IncludesFalAudioRow() Assert.AreEqual("audio", row.Kind); } + [Test] + public void List_IncludesMiniMaxCoverCapabilities() + { + ProviderInfo row = FindByIdKind("minimax", "audio"); + Assert.IsNotNull(row); + CollectionAssert.Contains(row.Capabilities, "music"); + CollectionAssert.Contains(row.Capabilities, "cover"); + CollectionAssert.Contains(row.Capabilities, "url"); + CollectionAssert.Contains(row.Capabilities, "base64"); + } + [Test] public void List_HasExactlyOneFalImage_AndOneFalAudio() { diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/GenerateAudioTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/GenerateAudioTests.cs index 748b9c34d..3d9228368 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/GenerateAudioTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/GenerateAudioTests.cs @@ -20,6 +20,7 @@ public void SetUp() { AssetGenJobManager.ResetForTests(); Environment.SetEnvironmentVariable("MCPFORUNITY_FAL_API_KEY", null); + Environment.SetEnvironmentVariable("MCPFORUNITY_MINIMAX_API_KEY", null); _dir = Path.Combine(Path.GetTempPath(), "mcp_audiohandler_" + Guid.NewGuid().ToString("N")); _store = new EncryptedFileKeyStore(_dir); SecureKeyStore.OverrideForTests(_store); @@ -70,6 +71,69 @@ public void Generate_UnknownProvider_ReturnsError() Assert.AreEqual(false, (bool)resp["success"]); } + [Test] + public void Generate_MiniMaxCover_WithReferenceUrl_ReturnsPendingJobId() + { + _store.Set("minimax", "test-provider-key"); + JObject gen = Call(new JObject + { + ["action"] = "generate", + ["provider"] = "minimax", + ["model"] = "music-cover", + ["prompt"] = "Warm acoustic folk cover", + ["audioUrl"] = "https://example.com/reference.mp3", + ["audioFormat"] = "mp3", + ["outputFormat"] = "url" + }); + Assert.AreEqual("pending", (string)gen["_mcp_status"]); + Assert.IsFalse(string.IsNullOrEmpty((string)gen["data"]["job_id"])); + } + + [Test] + public void Generate_MiniMaxCover_RequiresExactlyOneSource() + { + _store.Set("minimax", "test-provider-key"); + JObject none = Call(new JObject + { + ["action"] = "generate", + ["provider"] = "minimax", + ["prompt"] = "Warm acoustic folk cover" + }); + StringAssert.Contains("exactly one", ((string)none["error"]).ToLowerInvariant()); + + JObject two = Call(new JObject + { + ["action"] = "generate", + ["provider"] = "minimax", + ["prompt"] = "Warm acoustic folk cover", + ["audioUrl"] = "https://example.com/reference.mp3", + ["audioBase64"] = "SUQz" + }); + StringAssert.Contains("exactly one", ((string)two["error"]).ToLowerInvariant()); + } + + [Test] + public void Generate_MiniMaxCover_FeatureIdDoesNotReplaceReferenceAudio() + { + _store.Set("minimax", "test-provider-key"); + JObject missingSource = Call(new JObject + { + ["action"] = "generate", + ["provider"] = "minimax", + ["coverFeatureId"] = "feature-id" + }); + StringAssert.Contains("exactly one", ((string)missingSource["error"]).ToLowerInvariant()); + + JObject accepted = Call(new JObject + { + ["action"] = "generate", + ["provider"] = "minimax", + ["audioUrl"] = "https://example.com/reference.mp3", + ["coverFeatureId"] = "feature-id" + }); + Assert.AreEqual("pending", (string)accepted["_mcp_status"]); + } + [Test] public void ListProviders_FiltersAudioKind() { @@ -78,6 +142,7 @@ public void ListProviders_FiltersAudioKind() string s = resp.ToString(); StringAssert.Contains("fal", s); StringAssert.Contains("audio", s); + StringAssert.Contains("minimax", s); StringAssert.DoesNotContain("tripo", s); // model providers excluded StringAssert.DoesNotContain("openrouter", s); // image providers excluded } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs new file mode 100644 index 000000000..faf61a7fd --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs @@ -0,0 +1,199 @@ +using System; +using System.Text; +using System.Threading; +using MCPForUnity.Editor.Services.AssetGen.Http; +using MCPForUnity.Editor.Services.AssetGen.Providers; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace MCPForUnityTests.Editor.AssetGen +{ + public class MiniMaxAudioAdapterTests + { + private const string TestKey = "test-provider-key"; + + private static HttpResult Json(string body) + => new HttpResult { Status = 200, IsSuccess = true, Text = body }; + + private static HttpResult Completed(string audio) + => Json("{\"data\":{\"status\":2,\"audio\":\"" + audio + + "\"},\"base_resp\":{\"status_code\":0,\"status_msg\":\"success\"}}"); + + private static AudioGenRequest Request() + => new AudioGenRequest + { + Provider = "minimax", + Model = "music-cover", + Prompt = "Warm acoustic folk cover", + AudioUrl = "https://example.com/reference.mp3", + OutputFormat = "url", + AudioFormat = "mp3" + }; + + private static string SubmittedBody(FakeHttpTransport fake) + => Encoding.UTF8.GetString(fake.RecordedRequests[0].Body); + + [SetUp] + public void SetUp() => Environment.SetEnvironmentVariable(MiniMaxAudioAdapter.RegionEnvVar, null); + + [TearDown] + public void TearDown() => Environment.SetEnvironmentVariable(MiniMaxAudioAdapter.RegionEnvVar, null); + + [Test] + public void Submit_PostsGlobalCoverRequest_WithBearerAuthorization() + { + var fake = new FakeHttpTransport { Handler = _ => Completed("https://example.com/result.mp3") }; + var adapter = new MiniMaxAudioAdapter(); + + adapter.SubmitAsync(Request(), TestKey, fake, CancellationToken.None).GetAwaiter().GetResult(); + + HttpRequestSpec sent = fake.RecordedRequests[0]; + Assert.AreEqual("POST", sent.Method); + Assert.AreEqual(MiniMaxAudioAdapter.GlobalEndpoint, sent.Url); + StringAssert.StartsWith("Bearer ", sent.Headers["Authorization"]); + JObject body = JObject.Parse(SubmittedBody(fake)); + Assert.AreEqual("music-cover", (string)body["model"]); + Assert.AreEqual("https://example.com/reference.mp3", (string)body["audio_url"]); + Assert.AreEqual(false, (bool)body["stream"]); + Assert.AreEqual("url", (string)body["output_format"]); + Assert.AreEqual("mp3", (string)body["audio_setting"]["format"]); + } + + [Test] + public void Submit_ChinaRegion_UsesChinaEndpoint_AndWatermarkField() + { + Environment.SetEnvironmentVariable(MiniMaxAudioAdapter.RegionEnvVar, "cn_zh"); + var request = Request(); + request.AigcWatermark = true; + var fake = new FakeHttpTransport { Handler = _ => Completed("494433") }; + + new MiniMaxAudioAdapter().SubmitAsync(request, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + Assert.AreEqual(MiniMaxAudioAdapter.ChinaEndpoint, fake.RecordedRequests[0].Url); + JObject body = JObject.Parse(SubmittedBody(fake)); + Assert.AreEqual(true, (bool)body["aigc_watermark"]); + } + + [Test] + public void Submit_FeatureIdAndRequestOptions_AreSentWithRawAudio() + { + var request = Request(); + request.CoverFeatureId = "feature-id"; + request.Lyrics = "[Verse]\nA rewritten verse"; + request.LyricsOptimizer = true; + request.IsInstrumental = false; + var fake = new FakeHttpTransport { Handler = _ => Completed("494433") }; + + new MiniMaxAudioAdapter().SubmitAsync(request, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + JObject body = JObject.Parse(SubmittedBody(fake)); + Assert.AreEqual("feature-id", (string)body["cover_feature_id"]); + Assert.AreEqual("[Verse]\nA rewritten verse", (string)body["lyrics"]); + Assert.AreEqual(true, (bool)body["lyrics_optimizer"]); + Assert.AreEqual(false, (bool)body["is_instrumental"]); + Assert.AreEqual("https://example.com/reference.mp3", (string)body["audio_url"]); + Assert.IsNull(body["audio_base64"]); + } + + [Test] + public void Submit_PromptIsOptional() + { + var request = Request(); + request.Prompt = null; + var fake = new FakeHttpTransport { Handler = _ => Completed("494433") }; + + new MiniMaxAudioAdapter().SubmitAsync(request, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + Assert.IsNull(JObject.Parse(SubmittedBody(fake))["prompt"]); + } + + [Test] + public void Submit_Base64Source_IsSent() + { + var request = Request(); + request.AudioUrl = null; + request.AudioBase64 = "SUQz"; + var fake = new FakeHttpTransport { Handler = _ => Completed("494433") }; + + new MiniMaxAudioAdapter().SubmitAsync(request, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + Assert.AreEqual("SUQz", (string)JObject.Parse(SubmittedBody(fake))["audio_base64"]); + } + + [TestCase("mp3")] + [TestCase("wav")] + [TestCase("pcm")] + public void Poll_HexResult_DecodesAndPreservesAudioFormat(string format) + { + var request = Request(); + request.OutputFormat = "hex"; + request.AudioFormat = format; + var fake = new FakeHttpTransport { Handler = _ => Completed("494433") }; + var adapter = new MiniMaxAudioAdapter(); + + string id = adapter.SubmitAsync(request, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + ProviderPollResult result = adapter.PollAsync(id, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + Assert.AreEqual(ProviderPollState.Succeeded, result.State); + CollectionAssert.AreEqual(new byte[] { 0x49, 0x44, 0x33 }, result.InlineData); + Assert.AreEqual(format, result.ResultExt); + } + + [Test] + public void Poll_UrlResult_ReturnsDownloadUrl() + { + var fake = new FakeHttpTransport { Handler = _ => Completed("https://example.com/result.mp3") }; + var adapter = new MiniMaxAudioAdapter(); + + string id = adapter.SubmitAsync(Request(), TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + ProviderPollResult result = adapter.PollAsync(id, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + Assert.AreEqual(ProviderPollState.Succeeded, result.State); + Assert.AreEqual("https://example.com/result.mp3", result.DownloadUrl); + Assert.AreEqual("mp3", result.ResultExt); + } + + [Test] + public void Submit_NonZeroResponseCode_FailsAndRedactsKey() + { + var fake = new FakeHttpTransport + { + Handler = _ => Json("{\"base_resp\":{\"status_code\":1004,\"status_msg\":\"bad " + TestKey + "\"}}") + }; + var adapter = new MiniMaxAudioAdapter(); + + string id = adapter.SubmitAsync(Request(), TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + ProviderPollResult result = adapter.PollAsync(id, TestKey, fake, CancellationToken.None) + .GetAwaiter().GetResult(); + + Assert.AreEqual(ProviderPollState.Failed, result.State); + StringAssert.DoesNotContain(TestKey, result.Error); + } + + [Test] + public void Submit_RejectsMissingOrConflictingCoverSources() + { + var request = Request(); + request.AudioUrl = null; + var adapter = new MiniMaxAudioAdapter(); + var fake = new FakeHttpTransport(); + + Assert.Throws(() => adapter.SubmitAsync( + request, TestKey, fake, CancellationToken.None).GetAwaiter().GetResult()); + + request.AudioUrl = "https://example.com/reference.mp3"; + request.AudioBase64 = "SUQz"; + Assert.Throws(() => adapter.SubmitAsync( + request, TestKey, fake, CancellationToken.None).GetAwaiter().GetResult()); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs.meta new file mode 100644 index 000000000..706788edf --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc3f40ccdaa241979023e3ab09a9cd23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/website/docs/reference/tools/asset_gen/generate_audio.md b/website/docs/reference/tools/asset_gen/generate_audio.md index ad1df7f0b..7475e3229 100644 --- a/website/docs/reference/tools/asset_gen/generate_audio.md +++ b/website/docs/reference/tools/asset_gen/generate_audio.md @@ -1,7 +1,7 @@ --- title: generate_audio sidebar_label: generate_audio -description: "Generate audio (sound effects and background music) with fal.ai models and import them as AudioClips into the Unity project." +description: "Generate audio and import it as an AudioClip into the Unity project." --- # `generate_audio` @@ -12,12 +12,10 @@ description: "Generate audio (sound effects and background music) with fal.ai mo ## Description -Generate audio (sound effects and background music) with fal.ai models and import them as AudioClips into the Unity project. Bring-your-own-key: the fal key lives in the editor's secure store (shared with image generation) and never crosses the bridge. - -MODELS (all via fal.ai): fal-ai/stable-audio-25/text-to-audio (music + SFX, <=190s), cassetteai/sound-effects-generator (SFX, <=30s), cassetteai/music-generator (music), fal-ai/lyria2 (music). Omit model to use the model selected in the MCP for Unity -> Asset Generation tab. +Generate audio and import it as an AudioClip into the Unity project. Provider keys live in the editor's secure store and never cross the bridge. Omit model to use the model selected in the MCP for Unity -> Asset Generation tab. ACTIONS: -- generate: Submit an audio job from a text prompt. Returns { job_id }; poll with the status action. Params: provider (fal), prompt, model, duration (seconds), name, output_folder. +- generate: Submit an audio job. Cover models require one of audio_url or audio_base64 and also accept cover_feature_id. Returns { job_id }; poll with the status action. URL results expire after 24 hours. - status: Poll an async job by job_id -> { state, progress, assetPath?, error? }. - cancel: Cancel an in-flight job by job_id. - list_providers: List configured audio providers and capabilities (no key values). @@ -27,10 +25,19 @@ ACTIONS: | Name | Type | Required | Description | |------|------|----------|-------------| | `action` | `Literal['generate', 'status', 'cancel', 'list_providers']` | yes | Action to perform. | -| `provider` | `str \| None` | — | Provider id (fal). | +| `provider` | `str \| None` | — | Provider id. | | `prompt` | `str \| None` | — | Text prompt describing the sound or music. | -| `model` | `str \| None` | — | fal model id (e.g. fal-ai/stable-audio-25/text-to-audio). Omit to use the GUI-selected default. | +| `model` | `str \| None` | — | Provider model id. Omit to use the GUI-selected default. | | `duration` | `float \| None` | — | Requested length in seconds (soft-clamped per model). | +| `lyrics` | `str \| None` | — | Optional lyrics for a cover. | +| `lyrics_optimizer` | `bool \| None` | — | Whether to optimize the supplied lyrics. | +| `is_instrumental` | `bool \| None` | — | Whether to generate an instrumental cover. | +| `audio_url` | `str \| None` | — | Reference-audio URL for a cover (6-360 seconds, at most 50 MB). | +| `audio_base64` | `str \| None` | — | Base64 reference audio for a cover (6-360 seconds, at most 50 MB). | +| `cover_feature_id` | `str \| None` | — | Optional preprocessed cover feature id. | +| `output_format` | `Literal['url', 'hex'] \| None` | — | Provider response format; URL results expire after 24 hours. | +| `audio_format` | `Literal['mp3', 'wav', 'pcm'] \| None` | — | Generated audio encoding. | +| `aigc_watermark` | `bool \| None` | — | Whether to add the regional AIGC watermark. | | `name` | `str \| None` | — | Base name for the imported asset. | | `output_folder` | `str \| None` | — | Destination folder under Assets/ for the import. | | `job_id` | `str \| None` | — | Job id for status/cancel. | diff --git a/website/docs/reference/tools/asset_gen/index.md b/website/docs/reference/tools/asset_gen/index.md index a13023ee8..8a1f4dd5b 100644 --- a/website/docs/reference/tools/asset_gen/index.md +++ b/website/docs/reference/tools/asset_gen/index.md @@ -8,7 +8,7 @@ description: "MCP for Unity tools in the asset_gen group." AI asset generation – 3D model gen/import, 2D image gen & audio gen (bring-your-own-key) -- **[`generate_audio`](./generate_audio.md)** — Generate audio (sound effects and background music) with fal.ai models and import them as AudioClips into the Unity project. +- **[`generate_audio`](./generate_audio.md)** — Generate audio and import it as an AudioClip into the Unity project. - **[`generate_image`](./generate_image.md)** — Generate 2D images with AI providers (fal.ai, OpenRouter) and import them as textures/sprites into the Unity project. - **[`generate_model`](./generate_model.md)** — Generate 3D models with AI providers (Tripo, Meshy) and import them into the Unity project. - **[`import_model`](./import_model.md)** — Import 3D models from the Sketchfab marketplace into the Unity project. diff --git a/website/docs/reference/tools/index.md b/website/docs/reference/tools/index.md index a7466a134..a7de55db3 100644 --- a/website/docs/reference/tools/index.md +++ b/website/docs/reference/tools/index.md @@ -18,7 +18,7 @@ Animator control & AnimationClip creation ## `asset_gen`   (5 tools) AI asset generation – 3D model gen/import, 2D image gen & audio gen (bring-your-own-key) -- **[`generate_audio`](./asset_gen/generate_audio.md)** — Generate audio (sound effects and background music) with fal.ai models and import them as AudioClips into the Unity project. +- **[`generate_audio`](./asset_gen/generate_audio.md)** — Generate audio and import it as an AudioClip into the Unity project. - **[`generate_image`](./asset_gen/generate_image.md)** — Generate 2D images with AI providers (fal.ai, OpenRouter) and import them as textures/sprites into the Unity project. - **[`generate_model`](./asset_gen/generate_model.md)** — Generate 3D models with AI providers (Tripo, Meshy) and import them into the Unity project. - **[`import_model`](./asset_gen/import_model.md)** — Import 3D models from the Sketchfab marketplace into the Unity project.