MiniMax Music 3
A song from a description and its lyrics, sung, in stereo at 44.1 kHz. Leave the lyrics out and the track comes back instrumental.
What runs
This is the whole app. The entry names the model and what serves it, and the build turns that into these files: there is nothing else to write.
"""MiniMax Music 3, generated from a declarative template."""
from __future__ import annotations
import base64
import tempfile
from pydantic import BaseModel, ConfigDict, Field
from runware_serverless import serve
MODEL = "MiniMaxAI/MiniMax-Music3"
REVISION = "1b525ccb685ae809f33027c51c351eeb5833cf72"
CACHE = "/weights"
DEFAULT_DURATION = 60
class Request(BaseModel):
# An input this model does not read is a line the caller believed was doing
# something, so it is refused rather than dropped. Without this, pydantic
# ignores it: a width sent to a model of fixed size, or a misspelled seed,
# both come back as a result that silently answered a different request.
model_config = ConfigDict(extra="forbid")
prompt: str = Field(description="The style, instrumentation and mood to write in.")
lyrics: str = Field(default="", description="The words to sing. Left empty, the track is instrumental.")
duration: float = Field(
default=DEFAULT_DURATION,
ge=10,
le=240,
description="How long the track runs, in seconds.",
)
seed: int | None = Field(default=None, description="Repeat a result by repeating its seed.")
class Generated(BaseModel):
audio: str = Field(description="The result as a base64 WAV, stereo.")
duration: float
sample_rate: int
seed: int | None
@serve
class GeneratedMusic:
def load(self) -> None:
if hasattr(self, "pipeline"):
return
# Imported here rather than at the top of the file. The build imports
# this file with the platform's own dependencies, so nothing above
# exists until a worker runs.
import torch
from diffusers import ModularPipeline
self.torch = torch
self.pipeline = ModularPipeline.from_pretrained(
MODEL, revision=REVISION, cache_dir=CACHE
)
# Every component stays on the card. A worker's host RAM is half its
# card's VRAM, so parking anything on the host runs out of memory long
# before the card does, and the kill arrives as the container's exit 137
# rather than as a CUDA error.
self.pipeline.load_components(dtype=torch.bfloat16, device_map="cuda")
# load_components reports a component it could not build as a warning
# and leaves it None, so a wrong argument above does not fail the load:
# it surfaces one request later as an attribute error on None, far from
# its cause. Fail here instead, naming what is missing. Every declared
# component is meant to load, because this call names no workflow.
missing = self.pipeline.null_component_names
if missing:
raise RuntimeError(
"these components did not load, and the warnings above this line"
" say why: " + ", ".join(missing)
)
def predict(self, request: Request) -> Generated:
import soundfile
generator = None
if request.seed is not None:
generator = self.torch.Generator().manual_seed(request.seed)
results = self.pipeline(
prompt=request.prompt,
lyrics=request.lyrics,
audio_duration=request.duration,
generator=generator,
output_type="np",
)
track = _audios(results)[0]
rate = self.pipeline.sampling_rate
# soundfile writes frames by channel, and the pipeline hands back
# channels by frames, so the track arrives transposed or it is written as
# one channel of twice the length.
with tempfile.NamedTemporaryFile(suffix=".wav") as handle:
soundfile.write(handle.name, track.T, rate, format="WAV")
handle.seek(0)
encoded = base64.b64encode(handle.read()).decode("ascii")
# What came out, not what was asked for: the model rounds the length to
# its own frame rate.
return Generated(
audio=encoded,
duration=track.shape[-1] / rate,
sample_rate=rate,
seed=request.seed,
)
def _audios(results: object) -> object:
"""The waveform, however the pipeline hands it back."""
if isinstance(results, dict):
return results["audios"]
if hasattr(results, "audios"):
return results.audios
return resultsconfigVersion: 1
id: minimax-music3
name: MiniMax Music 3
source:
type: code
entry: model.py
compute:
gpuType: l40s
build:
requirements:
- torch==2.14.1
- torchvision==0.29.1
- diffusers==0.42.1
- transformers==5.21.0
- accelerate==1.15.0
- soundfile==0.14.0
volumes:
- /weightsDeploying
Deploy it as it is, or take the files above and deploy your own copy. Nothing you change reaches back to this page.
runware serverless deploy --from https://runware.ai/docs/serverless/examples/minimax-music3.ziprunware serverless deploy model.py \
--id minimax-music3 \
--gpu-type l40s \
--requirement torch==2.14.1 \
--requirement torchvision==0.29.1 \
--requirement diffusers==0.42.1 \
--requirement transformers==5.21.0 \
--requirement accelerate==1.15.0 \
--requirement soundfile==0.14.0 \
--volume /weightsCalling it
One request that works, against the app you just deployed. The fields are the ones the entry declares above, and anything it does not declare is refused rather than ignored.
import { createClient } from '@runware/sdk'
const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })
const task = await client.invoke({
appId: 'minimax-music3',
endpointPath: 'predict',
payload: {
request: {
prompt: 'a slow acoustic ballad, brushed drums, warm upright bass'
}
}
})import asyncio
import os
from runware import Runware
async def main():
client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
try:
task = await client.invoke(
"minimax-music3",
"predict",
{
"request": {
"prompt": "a slow acoustic ballad, brushed drums, warm upright bass"
}
},
)
finally:
await client.close()
asyncio.run(main())curl https://api.serverless.runware.ai/v1/apps/minimax-music3/invoke-async/predict \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"request": {
"prompt": "a slow acoustic ballad, brushed drums, warm upright bass"
}
}
}'cat <<'EOF' | runware serverless apps invoke minimax-music3 predict -f -
{
"request": {
"prompt": "a slow acoustic ballad, brushed drums, warm upright bass"
}
}
EOF{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"request": {
"prompt": "a slow acoustic ballad, brushed drums, warm upright bass"
}
}
}What it costs
This entry is pinned to l40s, which is the card it was tested on rather than the only one it runs on: --gpu-typedeploys it somewhere else. You pay for that card by the second while a worker holds it, and for nothing else. Pricing covers what meters, and the rates are published per GPU-second.