447 lines
16 KiB
Python
447 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""
|
|
This example shows how to use vLLM for running offline inference
|
|
with the correct prompt format on MiMo-Audio-Omni.
|
|
"""
|
|
|
|
import copy
|
|
import json
|
|
import os
|
|
from typing import NamedTuple
|
|
|
|
import soundfile as sf
|
|
from message_convert import (
|
|
get_audio_data,
|
|
get_audio_understanding_sft_prompt,
|
|
get_s2t_dialogue_sft_multiturn_prompt,
|
|
get_spoken_dialogue_sft_multiturn_prompt,
|
|
get_text_dialogue_sft_multiturn_prompt,
|
|
get_tts_sft_prompt,
|
|
to_prompt,
|
|
)
|
|
from vllm import SamplingParams
|
|
|
|
from vllm_omni.entrypoints.omni import Omni
|
|
from vllm_omni.inputs.data import OmniTokensPrompt
|
|
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
|
|
|
|
SEED = 42
|
|
|
|
MAX_CODE2WAV_TOKENS = 18192 # Maximum tokens supported by code2wav model
|
|
|
|
|
|
class QueryResult(NamedTuple):
|
|
inputs: dict
|
|
limit_mm_per_prompt: dict[str, int]
|
|
|
|
|
|
def get_codes_query_from_json(codes_path: str) -> QueryResult:
|
|
with open(codes_path, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
if isinstance(data, list):
|
|
code_final = data
|
|
elif isinstance(data, dict) and "code_final" in data:
|
|
code_final = data["code_final"]
|
|
else:
|
|
raise ValueError(
|
|
f"Unsupported codes json format in {codes_path}.\n"
|
|
"Expect a JSON list[int] or {{'code_final': list[int]}}."
|
|
)
|
|
|
|
if not isinstance(code_final, list) or not all(isinstance(x, int) for x in code_final):
|
|
raise ValueError("code_final must be a list[int].")
|
|
|
|
if len(code_final) > MAX_CODE2WAV_TOKENS:
|
|
print(f"[Warn] code_final len={len(code_final)} > {MAX_CODE2WAV_TOKENS}, truncating.")
|
|
code_final = code_final[:MAX_CODE2WAV_TOKENS]
|
|
|
|
return QueryResult(
|
|
inputs=OmniTokensPrompt(
|
|
prompt_token_ids=code_final,
|
|
multi_modal_data=None,
|
|
mm_processor_kwargs=None,
|
|
),
|
|
limit_mm_per_prompt={},
|
|
)
|
|
|
|
|
|
def get_tts_sft(
|
|
text="The weather is so nice today.",
|
|
instruct=None,
|
|
read_text_only=True,
|
|
prompt_speech=None,
|
|
audio_list=None,
|
|
):
|
|
res = get_tts_sft_prompt(
|
|
text,
|
|
instruct=instruct,
|
|
read_text_only=read_text_only,
|
|
prompt_speech=prompt_speech,
|
|
)
|
|
|
|
prompt = to_prompt(res)
|
|
final_prompt = {
|
|
"prompt": prompt,
|
|
}
|
|
if audio_list is not None:
|
|
final_prompt.update(
|
|
{
|
|
"multi_modal_data": {
|
|
"audio": audio_list,
|
|
},
|
|
}
|
|
)
|
|
return final_prompt
|
|
|
|
|
|
def get_audio_understanding_sft(audio_path, text="", thinking=False, use_sostm=False):
|
|
audio_list = []
|
|
audio_list.append(get_audio_data(audio_path))
|
|
res = get_audio_understanding_sft_prompt(
|
|
input_speech=audio_path, input_text=text, thinking=thinking, use_sostm=use_sostm
|
|
)
|
|
prompt = to_prompt(res)
|
|
final_prompt = {
|
|
"prompt": prompt,
|
|
"multi_modal_data": {
|
|
"audio": audio_list,
|
|
},
|
|
}
|
|
return final_prompt
|
|
|
|
|
|
def get_spoken_dialogue_sft_multiturn(message_list, system_prompt=None, ref_audio_path=None, audio_list=None):
|
|
res = get_spoken_dialogue_sft_multiturn_prompt(
|
|
message_list, system_prompt=system_prompt, prompt_speech=ref_audio_path
|
|
)
|
|
prompt = to_prompt(res)
|
|
final_prompt = {
|
|
"prompt": prompt,
|
|
"multi_modal_data": {
|
|
"audio": audio_list,
|
|
},
|
|
}
|
|
return final_prompt
|
|
|
|
|
|
def get_speech2text_dialogue_sft_multiturn(message_list, thinking=False, audio_list=None):
|
|
res = get_s2t_dialogue_sft_multiturn_prompt(
|
|
message_list,
|
|
thinking=thinking,
|
|
)
|
|
prompt = to_prompt(res)
|
|
final_prompt = {
|
|
"prompt": prompt,
|
|
"multi_modal_data": {
|
|
"audio": audio_list,
|
|
},
|
|
}
|
|
return final_prompt
|
|
|
|
|
|
def get_text_dialogue_sft_multiturn(
|
|
message_list,
|
|
):
|
|
res = get_text_dialogue_sft_multiturn_prompt(
|
|
message_list,
|
|
)
|
|
prompt = to_prompt(res)
|
|
final_prompt = {
|
|
"prompt": prompt,
|
|
}
|
|
return final_prompt
|
|
|
|
|
|
query_map = {
|
|
"tts_sft": get_tts_sft,
|
|
"tts_sft_with_instruct": get_tts_sft,
|
|
"tts_sft_with_audio": get_tts_sft,
|
|
"tts_sft_with_natural_instruction": get_tts_sft,
|
|
"audio_trancribing_sft": get_audio_understanding_sft,
|
|
"audio_understanding_sft": get_audio_understanding_sft,
|
|
"audio_understanding_sft_with_thinking": get_audio_understanding_sft,
|
|
"spoken_dialogue_sft_multiturn": get_spoken_dialogue_sft_multiturn,
|
|
"speech2text_dialogue_sft_multiturn": get_speech2text_dialogue_sft_multiturn,
|
|
"text_dialogue_sft_multiturn": get_text_dialogue_sft_multiturn,
|
|
}
|
|
|
|
|
|
def main(args):
|
|
model_name = args.model_name
|
|
|
|
# Get paths from args
|
|
text = getattr(args, "text", None)
|
|
audio_path = getattr(args, "audio_path", None)
|
|
|
|
instruct = getattr(args, "instruct", None)
|
|
|
|
# Get the query function and call it with appropriate parameters
|
|
query_func = query_map[args.query_type]
|
|
|
|
omni = Omni(
|
|
model=model_name,
|
|
deploy_config=args.deploy_config,
|
|
log_stats=args.enable_stats,
|
|
log_file=("omni_pipeline.log" if args.enable_stats else None),
|
|
init_sleep_seconds=args.init_sleep_seconds,
|
|
batch_timeout=args.batch_timeout,
|
|
init_timeout=args.init_timeout,
|
|
shm_threshold_bytes=args.shm_threshold_bytes,
|
|
)
|
|
|
|
thinker_sampling_params = SamplingParams(
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
top_k=-1,
|
|
max_tokens=2048,
|
|
seed=SEED,
|
|
logit_bias={},
|
|
repetition_penalty=1.1,
|
|
)
|
|
|
|
code2wav_sampling_params = SamplingParams(
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
top_k=-1,
|
|
max_tokens=4096 * 16,
|
|
seed=SEED,
|
|
detokenize=True,
|
|
repetition_penalty=1.1,
|
|
)
|
|
|
|
sampling_params_list = [
|
|
thinker_sampling_params,
|
|
code2wav_sampling_params,
|
|
]
|
|
|
|
# Build query result based on query type
|
|
# Notice: The audio files used in this example are available at: https://github.com/XiaomiMiMo/MiMo-Audio/tree/main/examples
|
|
if args.query_type == "tts_sft":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft
|
|
query_result = query_func(text=text, read_text_only=True)
|
|
elif args.query_type == "tts_sft_with_instruct":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft_with_instruct --instruct "Speak happily in a child's voice"
|
|
query_result = query_func(text=text, instruct=instruct, read_text_only=True)
|
|
elif args.query_type == "tts_sft_with_audio":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft_with_audio --audio_path "./spoken_dialogue_assistant_turn_1.wav"
|
|
audio_list = [get_audio_data(audio_path)]
|
|
query_result = query_func(text=text, read_text_only=True, prompt_speech=audio_path, audio_list=audio_list)
|
|
elif args.query_type == "tts_sft_with_natural_instruction":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft_with_natural_instruction --text "In a panting young male voice, he said: I can't run anymore, wait for me!"
|
|
query_result = query_func(text=text, read_text_only=False)
|
|
elif args.query_type == "audio_trancribing_sft":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type audio_trancribing_sft --audio_path "./spoken_dialogue_assistant_turn_1.wav"
|
|
audio_path = "spoken_dialogue_assistant_turn_1.wav"
|
|
text = "Please transcribe this audio and repeat it once."
|
|
query_result = query_func(text=text, audio_path=audio_path, use_sostm=True)
|
|
elif args.query_type == "audio_understanding_sft":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type audio_understanding_sft --text "Summarize the audio." --audio_path "./spoken_dialogue_assistant_turn_1.wav"
|
|
query_result = query_func(text=text, audio_path=audio_path)
|
|
elif args.query_type == "audio_understanding_sft_with_thinking":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type audio_understanding_sft_with_thinking --text "Summarize the audio." --audio_path "./spoken_dialogue_assistant_turn_1.wav"
|
|
query_result = query_func(text=text, audio_path=audio_path, thinking=True)
|
|
elif args.query_type == "spoken_dialogue_sft_multiturn":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type spoken_dialogue_sft_multiturn --audio_path "./prompt_speech_zh_m.wav"
|
|
first_turn_text_response = "我没办法获取实时的天气信息。不过呢,你可以试试几个方法来查看今天的天气。首先,你可以用手机自带的天气功能,比如苹果手机的天气应用,或者直接在系统设置里查看。其次,你也可以用一些专业的天气服务,像是国外的AccuWeather、Weather.com,或者国内的中国天气网、墨迹天气等等。再有就是,你还可以在谷歌或者百度里直接搜索你所在的城市加上天气这两个字。如果你能告诉我你所在的城市,我也可以帮你分析一下历史天气趋势,不过最新的数据还是需要你通过官方渠道去获取哦。"
|
|
audio_list = []
|
|
s1_audio_path = "weather_of_today.mp3"
|
|
s2_audio_path = "spoken_dialogue_assistant_turn_1.wav"
|
|
s3_audio_path = "beijing.mp3"
|
|
audio_list.append(get_audio_data(audio_path))
|
|
audio_list.append(get_audio_data(s1_audio_path))
|
|
audio_list.append(get_audio_data(s2_audio_path))
|
|
audio_list.append(get_audio_data(s3_audio_path))
|
|
|
|
message_list = [
|
|
{"role": "user", "content": s1_audio_path},
|
|
{"role": "assistant", "content": {"text": first_turn_text_response, "audio": s2_audio_path}},
|
|
{"role": "user", "content": s3_audio_path},
|
|
]
|
|
query_result = query_func(message_list, system_prompt=None, ref_audio_path=audio_path, audio_list=audio_list)
|
|
elif args.query_type == "speech2text_dialogue_sft_multiturn":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type speech2text_dialogue_sft_multiturn
|
|
s1_audio_path = "weather_of_today.mp3"
|
|
s2_audio_path = "beijing.mp3"
|
|
audio_list = []
|
|
audio_list.append(get_audio_data(s1_audio_path))
|
|
audio_list.append(get_audio_data(s2_audio_path))
|
|
message_list = [
|
|
{"role": "user", "content": s1_audio_path},
|
|
{
|
|
"role": "assistant",
|
|
"content": "Hello, I can't get real-time weather information. If you can tell me your city, I can help you analyze historical weather trends, but for the latest data, you'll need to get it through official channels.",
|
|
},
|
|
{"role": "user", "content": s2_audio_path},
|
|
]
|
|
query_result = query_func(message_list, thinking=True, audio_list=audio_list)
|
|
elif args.query_type == "text_dialogue_sft_multiturn":
|
|
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type text_dialogue_sft_multiturn
|
|
message_list = [
|
|
{"role": "user", "content": "Could you recommend some tourist attractions in China?"},
|
|
{"role": "assistant", "content": "Hello, which city would you like to travel to?"},
|
|
{"role": "user", "content": "Beijing"},
|
|
]
|
|
query_result = query_func(message_list=message_list)
|
|
else:
|
|
raise ValueError(f"Invalid query type: {args.query_type}")
|
|
|
|
prompts = [copy.deepcopy(query_result) for _ in range(args.num_prompts)]
|
|
|
|
print("prompts", prompts)
|
|
omni_outputs = omni.generate(prompts, sampling_params_list)
|
|
|
|
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
|
|
if args.query_type is not None:
|
|
output_dir = os.path.join(output_dir, args.query_type)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
for stage_outputs in omni_outputs:
|
|
output = stage_outputs.request_output
|
|
if stage_outputs.final_output_type == "text":
|
|
request_id = output.request_id
|
|
text_output = output.outputs[0].text
|
|
# Save aligned text file per request
|
|
prompt_text = output.prompt
|
|
out_txt = os.path.join(output_dir, f"{request_id}.txt")
|
|
lines = []
|
|
lines.append("Prompt:\n")
|
|
lines.append(str(prompt_text) + "\n")
|
|
lines.append("vllm_text_output:\n")
|
|
output_text = str(text_output)
|
|
if "<chinese>" in output_text or "<english>" in output_text:
|
|
output_text = output_text.replace("<chinese>", "").replace("<english>", "").strip()
|
|
lines.append(output_text + "\n")
|
|
try:
|
|
with open(out_txt, "w", encoding="utf-8") as f:
|
|
print("lines", lines)
|
|
f.writelines(lines)
|
|
except Exception as e:
|
|
print(f"[Warn] Failed writing text file {out_txt}: {e}")
|
|
print(f"Request ID: {request_id}, Text saved to {out_txt}\n")
|
|
elif stage_outputs.final_output_type == "audio":
|
|
request_id = output.request_id
|
|
audio_tensor = output.outputs[0].multimodal_output.get("audio")
|
|
|
|
if audio_tensor is None:
|
|
continue
|
|
|
|
output_wav = os.path.join(output_dir, f"{request_id}.wav")
|
|
|
|
# Convert to numpy array and ensure correct format
|
|
audio_numpy = audio_tensor.float().detach().cpu().numpy()
|
|
|
|
# Ensure audio is 1D (flatten if needed)
|
|
if audio_numpy.ndim > 1:
|
|
audio_numpy = audio_numpy.flatten()
|
|
|
|
# Save audio file with explicit WAV format
|
|
sf.write(output_wav, audio_numpy, samplerate=24000, format="WAV")
|
|
print(f"Request ID: {request_id}, Audio saved to {output_wav}")
|
|
|
|
|
|
def parse_args():
|
|
parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
|
|
parser.add_argument(
|
|
"--model-name",
|
|
"-m",
|
|
type=str,
|
|
default="XiaomiMiMo/MiMo-Audio-7B-Instruct",
|
|
help="Backbone LLM path.",
|
|
)
|
|
parser.add_argument(
|
|
"--text",
|
|
"-t",
|
|
type=str,
|
|
default="",
|
|
help="input text",
|
|
)
|
|
parser.add_argument(
|
|
"--query-type",
|
|
"-q",
|
|
type=str,
|
|
default="tts_sft",
|
|
choices=query_map.keys(),
|
|
help="Query type.",
|
|
)
|
|
parser.add_argument(
|
|
"--audio-path",
|
|
"-a",
|
|
type=str,
|
|
default=None,
|
|
help="Path to local audio file. If not provided, uses default audio asset.",
|
|
)
|
|
parser.add_argument(
|
|
"--instruct",
|
|
type=str,
|
|
default=None,
|
|
help="instruct",
|
|
)
|
|
parser.add_argument(
|
|
"--enable-stats",
|
|
action="store_true",
|
|
help="Enable writing detailed statistics (default: disabled)",
|
|
)
|
|
parser.add_argument(
|
|
"--init-sleep-seconds",
|
|
type=int,
|
|
default=20,
|
|
help="Sleep seconds after starting each stage process to allow initialization (default: 20)",
|
|
)
|
|
parser.add_argument(
|
|
"--batch-timeout",
|
|
type=int,
|
|
default=5,
|
|
help="Timeout for batching in seconds (default: 5)",
|
|
)
|
|
parser.add_argument(
|
|
"--init-timeout",
|
|
type=int,
|
|
default=5000,
|
|
help="Timeout for initializing stages in seconds (default: 5000)",
|
|
)
|
|
parser.add_argument(
|
|
"--shm-threshold-bytes",
|
|
type=int,
|
|
default=65536,
|
|
help="Threshold for using shared memory in bytes (default: 65536)",
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
default="./output_audio",
|
|
help="Output audio wav directory.",
|
|
)
|
|
parser.add_argument(
|
|
"--output-wav",
|
|
default="output_audio",
|
|
help="[Deprecated] Output wav directory (use --output-dir).",
|
|
)
|
|
parser.add_argument(
|
|
"--num-prompts",
|
|
type=int,
|
|
default=1,
|
|
help="Number of prompts to generate.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--sampling-rate",
|
|
type=int,
|
|
default=24000,
|
|
help="Sampling rate for audio.",
|
|
)
|
|
parser.add_argument(
|
|
"--deploy-config",
|
|
type=str,
|
|
default=None,
|
|
help="Override the deploy config path. If unset, auto-loads "
|
|
"vllm_omni/deploy/mimo_audio.yaml based on the HF model_type.",
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = parse_args()
|
|
main(args)
|