在 SageMaker AI 上部署 Qwen3-TTS:用 vLLM-Omni 构建实时语音流应用

2026-09-29 29 预计阅读时间: 1 分钟
来源: aws.amazon.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

文本转语音应用的体验差异,往往不在于模型能否生成音频,而在于用户要等多久才能听到第一个音节。将 Qwen3-TTS 部署到 Amazon SageMaker AI,并使用 AWS vLLM-Omni Deep Learning Container 持续返回音频块,可以让前端在整段语音生成完成之前就开始播放。

这类架构的重点不只是“部署一个模型端点”,还包括持久双向连接、音频分块、前端增量播放,以及连接中断和并发压力的处理。本文聚焦第一阶段:部署 TTS 模型,并通过 Gradio 构建一个可交互的流式语音界面。

请求—响应不够:为什么要使用持久连接

传统推理接口通常采用一次 HTTP 请求对应一次完整响应的模式:客户端提交文本,服务端生成整段 WAV 文件,然后一次性返回。实现简单,但首包延迟会随文本长度和生成耗时上升。

流式 TTS 则把过程拆成多个连续事件:

  1. 客户端发送文本、音色和采样参数。
  2. 推理服务开始生成音频 token 或波形块。
  3. 服务端立即推送已经生成的音频块。
  4. 客户端一边接收,一边解码和播放。
  5. 服务端发送完成事件,连接可以保留并处理后续请求。

一个典型的数据路径如下:

Browser
  │  Gradio 的持续连接
  ▼
Gradio application
  │  经过认证的双向流
  ▼
SageMaker endpoint
  │
  ▼
AWS vLLM-Omni container + Qwen3-TTS

不要让浏览器直接持有 AWS 长期凭证。更稳妥的方式是由 Gradio 后端或独立 API 网关负责身份验证、请求签名和上游连接,浏览器只连接应用层服务。

用可配置脚本创建 SageMaker 端点

实际使用的镜像 URI、Qwen3-TTS 模型 ID和容器环境变量,应以目标区域中的 AWS vLLM-Omni DLC 及其启动约定为准。下面的脚本故意不猜测容器内部的环境变量名称,而是通过 MODEL_ENV_JSON 注入,这样可以直接按所用镜像的要求调整。

先安装依赖:

python -m venv .venv
source .venv/bin/activate
pip install "sagemaker>=2.220,<3" boto3

创建 deploy.py:

import json
import os

import sagemaker
from sagemaker.model import Model

region = os.environ.get("AWS_REGION", "us-east-1")
role_arn = os.environ["SAGEMAKER_ROLE_ARN"]
image_uri = os.environ["VLLM_OMNI_IMAGE_URI"]
endpoint_name = os.environ.get("ENDPOINT_NAME", "qwen3-tts-vllm-omni")
instance_type = os.environ.get("INSTANCE_TYPE", "ml.g5.2xlarge")

# 示例:'{"MODEL_ID":"<qwen3-tts-model-id>","DTYPE":"bfloat16"}'
# 键名必须与实际 DLC 的启动契约一致。
container_env = json.loads(os.environ.get("MODEL_ENV_JSON", "{}"))
if not container_env:
    raise RuntimeError("请通过 MODEL_ENV_JSON 提供 DLC 所需的模型配置")

session = sagemaker.Session(boto_session=sagemaker.boto3.Session(region_name=region))

model = Model(
    image_uri=image_uri,
    role=role_arn,
    env=container_env,
    sagemaker_session=session,
)

model.deploy(
    initial_instance_count=1,
    instance_type=instance_type,
    endpoint_name=endpoint_name,
    container_startup_health_check_timeout=1800,
)

print(f"Endpoint ready: {endpoint_name}")

配置变量并部署:

export AWS_REGION=us-east-1
export SAGEMAKER_ROLE_ARN='arn:aws:iam::<account-id>:role/<sagemaker-execution-role>'
export VLLM_OMNI_IMAGE_URI='<regional-vllm-omni-dlc-image-uri>'
export ENDPOINT_NAME='qwen3-tts-vllm-omni'
export INSTANCE_TYPE='ml.g5.2xlarge'
export MODEL_ENV_JSON='{"MODEL_ID":"<qwen3-tts-model-id>","DTYPE":"bfloat16"}'

python deploy.py

运行前需要确认三件事:实例类型能够容纳模型;执行角色有拉取镜像和读取模型文件的权限;模型许可证允许目标使用场景。GPU 实例会持续计费,不测试时应删除端点,而不只是停止本地 Gradio 程序。

将音频块接入 Gradio

下面是一个可以改造的最小 Gradio 客户端。它假设上游暴露 WebSocket,并采用以下示例协议:客户端发送一条 JSON synthesize 消息,服务端持续返回 PCM16 单声道二进制块,最后返回 {"event":"done"}。

这是一个应用层协议示例,并不是对 SageMaker 或 DLC 固定报文格式的声明。接入实际端点时,需要按照所用连接器修改 URL、认证方式和消息字段;如果上游返回 WAV、Opus 或带元数据的帧,也要相应更换解码逻辑。

安装依赖:

pip install "gradio>=4.44,<6" "websockets>=12,<16" "numpy>=1.26,<3"

创建 app.py:

import asyncio
import json
import os

import gradio as gr
import numpy as np
import websockets

VOICE_WS_URL = os.environ.get("VOICE_WS_URL", "ws://127.0.0.1:8080/tts")
VOICE_AUTH_TOKEN = os.environ.get("VOICE_AUTH_TOKEN", "")
SAMPLE_RATE = int(os.environ.get("SAMPLE_RATE", "24000"))


async def synthesize(text: str, voice: str):
    text = text.strip()
    if not text:
        raise gr.Error("请输入要合成的文本")

    headers = {}
    if VOICE_AUTH_TOKEN:
        headers["Authorization"] = f"Bearer {VOICE_AUTH_TOKEN}"

    # websockets 14+ 使用 additional_headers。
    async with websockets.connect(
        VOICE_WS_URL,
        additional_headers=headers,
        open_timeout=20,
        ping_interval=20,
        max_size=None,
    ) as ws:
        await ws.send(json.dumps({
            "event": "synthesize",
            "text": text,
            "voice": voice,
            "format": "pcm_s16le",
            "sample_rate": SAMPLE_RATE,
        }))

        while True:
            message = await asyncio.wait_for(ws.recv(), timeout=60)

            if isinstance(message, bytes):
                pcm = np.frombuffer(message, dtype="<i2")
                # Gradio 可以消费 (采样率, NumPy 数组) 形式的流式音频。
                yield SAMPLE_RATE, pcm
                continue

            event = json.loads(message)
            if event.get("event") == "done":
                break
            if event.get("event") == "error":
                raise gr.Error(event.get("message", "上游语音生成失败"))


with gr.Blocks(title="Qwen3-TTS Streaming Demo") as demo:
    gr.Markdown("## Qwen3-TTS 实时语音生成")
    text = gr.Textbox(
        label="文本",
        lines=5,
        value="你好,这是一段边生成边播放的语音。",
    )
    voice = gr.Textbox(label="音色", value="default")
    generate = gr.Button("开始生成", variant="primary")
    audio = gr.Audio(label="流式音频", streaming=True, autoplay=True)

    generate.click(
        fn=synthesize,
        inputs=[text, voice],
        outputs=audio,
        concurrency_limit=4,
    )

if __name__ == "__main__":
    demo.queue(default_concurrency_limit=4).launch(
        server_name="0.0.0.0",
        server_port=7860,
    )

启动界面:

export VOICE_WS_URL='wss://<your-streaming-gateway>/tts'
export VOICE_AUTH_TOKEN='<short-lived-token>'
export SAMPLE_RATE=24000
python app.py

生产环境中,上游连接通常还需要 AWS SigV4 签名、临时凭证或由受控代理完成鉴权。不要为了快速演示而把 IAM Access Key 写进 JavaScript、镜像或 Git 仓库。

实时体验取决于整条链路

模型生成速度只是其中一个指标。调试时至少应记录以下延迟:

  • 连接建立时间:包括 DNS、TLS、认证和 WebSocket 握手。
  • 首音频块延迟:从提交文本到收到第一块可播放 PCM 的时间。
  • 实时系数:生成耗时与音频时长的比值;小于 1 才能持续赶上播放速度。
  • 块间抖动:音频块到达不均匀会造成停顿,即使平均吞吐量足够。
  • 端到端完成时间:用于容量规划和并发评估。

音频块不是越小越好。块太大会增加首包等待,块太小则会放大协议、Python 调度和网络开销。可以从 20~100 毫秒音频对应的数据量开始测试,再结合网络状况和播放器缓冲策略调整。这个区间是工程起点,不是 Qwen3-TTS 的固定要求。

还要为长文本设置上限。较稳妥的做法是按句子切分输入,在标点处排队生成,并保留少量播放缓冲。这样既能降低单次请求风险,也更容易取消尚未播放的后续内容。

上线前检查清单

将演示升级为服务时,建议逐项确认:

  • 容器镜像、模型权重和实例类型已经在同一区域验证。
  • Gradio 或网关使用短期凭证,浏览器无法接触 AWS 密钥。
  • 连接具备超时、心跳、取消和断线清理机制。
  • 客户端明确知道采样率、声道数、字节序和音频编码。
  • 端点设置并发上限、输入长度限制和请求级追踪 ID。
  • 监控首音频块延迟、错误率、GPU 利用率和活跃连接数。
  • 对用户输入、生成内容、音色授权和日志保留实施合规控制。
  • 测试结束后删除闲置 SageMaker 端点,避免 GPU 成本继续累积。

这一阶段解决的是“文本如何尽快变成可播放语音”。真正的双向语音助手还需要补上语音输入、端点检测、语音转写、打断控制和对话状态管理。先把 TTS 流稳定下来,再扩展完整语音回路,通常比一次性叠加所有组件更容易定位性能问题。


相关推荐