Signed-off-by: Celeste-jq <591998922@qq.com> Signed-off-by: lyj-jjj <liuyingjun5@huawei.com> Signed-off-by: IsleOfDawnlight <stellamou@qq.com> Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Celeste-jq <591998922@qq.com> Co-authored-by: lyj-jjj <liuyingjun5@huawei.com> Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
"""OpenAI-compatible client for VoxCPM via /v1/audio/speech.
|
|
|
|
Examples:
|
|
# Basic text-to-speech
|
|
python openai_speech_client.py --text "Hello from VoxCPM"
|
|
|
|
# Voice cloning
|
|
python openai_speech_client.py \
|
|
--text "This sentence uses the cloned voice." \
|
|
--ref-audio /path/to/reference.wav \
|
|
--ref-text "The exact transcript spoken in the reference audio."
|
|
|
|
# Streaming PCM output
|
|
python openai_speech_client.py \
|
|
--text "This is a streaming VoxCPM request." \
|
|
--stream \
|
|
--output output.pcm
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import os
|
|
|
|
import httpx
|
|
|
|
DEFAULT_API_BASE = "http://localhost:8091"
|
|
DEFAULT_API_KEY = "EMPTY"
|
|
DEFAULT_MODEL = "OpenBMB/VoxCPM1.5"
|
|
|
|
|
|
def encode_audio_to_base64(audio_path: str) -> str:
|
|
"""Encode a local audio file to base64 data URL."""
|
|
if not os.path.exists(audio_path):
|
|
raise FileNotFoundError(f"Audio file not found: {audio_path}")
|
|
|
|
ext = audio_path.lower().rsplit(".", 1)[-1]
|
|
mime_map = {
|
|
"wav": "audio/wav",
|
|
"mp3": "audio/mpeg",
|
|
"flac": "audio/flac",
|
|
"ogg": "audio/ogg",
|
|
}
|
|
mime_type = mime_map.get(ext, "audio/wav")
|
|
|
|
with open(audio_path, "rb") as f:
|
|
audio_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
return f"data:{mime_type};base64,{audio_b64}"
|
|
|
|
|
|
def build_payload(args) -> dict[str, object]:
|
|
payload: dict[str, object] = {
|
|
"model": args.model,
|
|
"input": args.text,
|
|
"response_format": "pcm" if args.stream else args.response_format,
|
|
}
|
|
|
|
if args.ref_audio:
|
|
if args.ref_audio.startswith(("http://", "https://", "data:")):
|
|
payload["ref_audio"] = args.ref_audio
|
|
else:
|
|
payload["ref_audio"] = encode_audio_to_base64(args.ref_audio)
|
|
if args.ref_text:
|
|
payload["ref_text"] = args.ref_text
|
|
if args.max_new_tokens is not None:
|
|
payload["max_new_tokens"] = args.max_new_tokens
|
|
if args.stream:
|
|
payload["stream"] = True
|
|
|
|
return payload
|
|
|
|
|
|
def run_tts(args) -> None:
|
|
payload = build_payload(args)
|
|
api_url = f"{args.api_base}/v1/audio/speech"
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Authorization": f"Bearer {args.api_key}",
|
|
}
|
|
|
|
print(f"Model: {args.model}")
|
|
print(f"Text: {args.text}")
|
|
if args.ref_audio:
|
|
print("Mode: voice cloning")
|
|
print(f"Reference audio: {args.ref_audio}")
|
|
else:
|
|
print("Mode: text-to-speech")
|
|
|
|
if args.stream:
|
|
output_path = args.output or "voxcpm_output.pcm"
|
|
with httpx.Client(timeout=300.0) as client:
|
|
with client.stream("POST", api_url, json=payload, headers=headers) as response:
|
|
if response.status_code != 200:
|
|
print(f"Error: {response.status_code}")
|
|
print(response.read().decode("utf-8", errors="ignore"))
|
|
return
|
|
|
|
total_bytes = 0
|
|
with open(output_path, "wb") as f:
|
|
for chunk in response.iter_bytes():
|
|
if not chunk:
|
|
continue
|
|
f.write(chunk)
|
|
total_bytes += len(chunk)
|
|
print(f"Streamed {total_bytes} bytes to: {output_path}")
|
|
return
|
|
|
|
with httpx.Client(timeout=300.0) as client:
|
|
response = client.post(api_url, json=payload, headers=headers)
|
|
|
|
if response.status_code != 200:
|
|
print(f"Error: {response.status_code}")
|
|
print(response.text)
|
|
return
|
|
|
|
try:
|
|
text = response.content.decode("utf-8")
|
|
if text.startswith('{"error"'):
|
|
print(f"Error: {text}")
|
|
return
|
|
except UnicodeDecodeError:
|
|
pass
|
|
|
|
output_path = args.output or "voxcpm_output.wav"
|
|
with open(output_path, "wb") as f:
|
|
f.write(response.content)
|
|
print(f"Audio saved to: {output_path}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="VoxCPM OpenAI-compatible speech client")
|
|
parser.add_argument("--api-base", default=DEFAULT_API_BASE, help="API base URL")
|
|
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key")
|
|
parser.add_argument("--model", "-m", default=DEFAULT_MODEL, help="Model name or path")
|
|
parser.add_argument("--text", required=True, help="Text to synthesize")
|
|
parser.add_argument("--ref-audio", default=None, help="Reference audio path, URL, or data URL")
|
|
parser.add_argument(
|
|
"--ref-text",
|
|
default=None,
|
|
help="The exact transcript spoken in the reference audio",
|
|
)
|
|
parser.add_argument("--stream", action="store_true", help="Enable streaming PCM output")
|
|
parser.add_argument(
|
|
"--response-format",
|
|
default="wav",
|
|
choices=["wav", "pcm", "flac", "mp3", "aac", "opus"],
|
|
help="Audio format for non-streaming mode (default: wav)",
|
|
)
|
|
parser.add_argument("--max-new-tokens", type=int, default=None, help="Maximum tokens to generate")
|
|
parser.add_argument("--output", "-o", default=None, help="Output file path")
|
|
args = parser.parse_args()
|
|
run_tts(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|