voicecraftali

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 25 Imported by: 0

README

voicecraft-ali

Go Reference Go Report Card

github.com/gtkit/go-voicecraft-ali 提供阿里云百炼(Model Studio)语音合成的 Go 客户端, 覆盖三套实时 WebSocket API、三套非实时 HTTP API、声音复刻与声音设计。

三套 WebSocket API 收敛到同一个 Session 类型上,方法集合与语义一致,切换 API 不需要改动读写代码。

go get github.com/gtkit/go-voicecraft-ali

要求 Go 1.27+。导入后的包名是 voicecraftali

import voicecraftali "github.com/gtkit/go-voicecraft-ali"

支持的 API

实时合成(WebSocket)
API 传输 会话入口 模型
CosyVoice / Qwen-Audio-TTS 双向流式 NewSynthesisSession qwen-audio-3.0-tts-*cosyvoice-v1 ~ v3.5
Sambert 单向流式 NewSambertSession sambert-*-v1
Qwen-TTS Realtime 事件流 NewRealtimeSession qwen3-tts-*-realtimeqwen-tts-realtime

三个入口返回的都是 *Session

非实时合成(HTTP)
API 非流式 流式(SSE) 模型
Qwen-Audio-TTS / CosyVoice SynthesizeHTTP StreamHTTP qwen-audio-3.0-tts-*cosyvoice-v2 ~ v3.5
Qwen-TTS SynthesizeQwenTTS StreamQwenTTS qwen3-tts-*qwen-tts
MiniMax SynthesizeMiniMax StreamMiniMax MiniMax/speech-2.8-hd

非实时接口仅在北京地域可用。

声音复刻与音色管理
模型家族 创建 列表 详情 更新 删除
Qwen-Audio-TTS / CosyVoice CreateVoice ListVoices GetVoice UpdateVoice DeleteVoice
Qwen-TTS CreateQwenVoice ListQwenVoices DeleteQwenVoice
MiniMax CloneMiniMaxVoice
声音设计
模型家族 创建 列表 详情 删除
Qwen-Audio-TTS / CosyVoice DesignVoice 复用 ListVoices 复用 GetVoice 复用 DeleteVoice
Qwen DesignQwenVoice ListDesignedQwenVoices GetDesignedQwenVoice DeleteDesignedQwenVoice

快速开始

流式合成——边送文本边取音频
client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
if err != nil {
    return err
}

session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
    Model:  voicecraftali.ModelCosyVoiceV3Flash,
    Voice:  "longanyang",
    Format: voicecraftali.FormatMP3,
})
if err != nil {
    return err
}
defer session.Close()

go func() {
    for _, line := range lines {
        if err := session.SendText(ctx, line); err != nil {
            return
        }
    }
    _ = session.Finish(ctx) // 送完必须 Finish,服务端据此输出剩余音频
}()

err = session.Stream(ctx, func(audio []byte) error {
    _, err := file.Write(audio)
    return err
})
一次性合成——短文本直接拿完整音频
audio, err := client.Synthesize(ctx, params, "床前明月光,疑是地上霜。")

对应另外两套 API 的是 SynthesizeSambertSynthesizeRealtime

拉模式读取
for {
    audio, err := session.Read()
    if errors.Is(err, io.EOF) {
        break // 合成结束
    }
    if err != nil {
        return err
    }
    process(audio)
}

ReadContext(ctx)Read 语义相同,但在等待音频时响应 ctx 取消。


三套 WebSocket API 的能力差异

差异由服务端协议决定,本库以 ErrUnsupportedOperation 显式暴露,不静默降级:

方法 CosyVoice / Qwen-Audio-TTS Sambert Qwen-TTS Realtime
SendText 支持,可多次调用 ErrUnsupportedOperation 支持,可多次调用
Commit ErrUnsupportedOperation ErrUnsupportedOperation 支持
Flush 支持 ErrUnsupportedOperation ErrUnsupportedOperation
Finish 发送 finish-task 空操作,返回 nil 发送 session.finish
Cancel 支持 ErrUnsupportedOperation ErrUnsupportedOperation(见 CancelResponse
CancelResponse ErrUnsupportedOperation ErrUnsupportedOperation 支持
Read / ReadContext / Stream / Close 支持 支持 支持

Flush 让服务端立即合成已送入但尚未合成的文本,不结束任务,因此调用后仍可继续 SendText。它不携带文本,既不计入累计字符额度,也不占用 SSML 模式的单次发送名额。 Qwen-TTS Realtime 用 Commit 达到同样目的。

CancelCancelResponse 的区别是终结性:Cancel 结束整个任务,调用后不能再 SendTextCancelResponse(Qwen-TTS Realtime 的 response.cancel)只取消当前 响应,会话继续可用。

Sambert 的特殊之处

Sambert 与 CosyVoice 共用端点,但它是另一套 API:

  • 没有 Voice 参数——音色由模型名决定(sambert-zhichu-v1 即知厨)
  • 全文在创建会话时提交:填 SambertParams.TextSendText 返回 ErrUnsupportedOperation
  • 音频格式为 pcm / wav / mp3,采样率为 8000 / 16000 / 22050 / 24000
  • 支持音素级时间戳(PhonemeTimestampEnabled,需同时开启 WordTimestampEnabled
  • 仅在北京地域可用
session, err := client.NewSambertSession(ctx, &voicecraftali.SambertParams{
    Model: voicecraftali.ModelSambertZhichu,
    Text:  "白日依山尽,黄河入海流。",
})
Qwen-TTS Realtime 的交互模式
  • ModeServerCommit(协议默认):服务端自动判定合成时机,持续 SendText 即可
  • ModeCommit:需要显式调用 Commit 才会触发合成,延迟最低

文本长度约束

CosyVoice / Qwen-Audio-TTS 的每次 SendText

  • 单次不超过 20000 字符(按 rune 计)
  • 单个任务累计不超过 200000 字符
  • 两次发送的间隔不超过 23 秒,否则服务端断开连接

超出前两条时 SendText 返回 ErrTextTooLong,请求不会离开进程。


鉴权

阿里云百炼使用 API Key 鉴权,凭据在 WebSocket 握手阶段校验。 北京地域与新加坡地域的 API Key 不通用,需与 WithRegion 匹配。

// 静态密钥
voicecraftali.New(voicecraftali.WithAPIKey("sk-xxx"))

// 环境变量:两个凭据选项都不配时,从 DASHSCOPE_API_KEY 读取
voicecraftali.New()

// 动态凭据:短期密钥、STS 临时凭证、密钥托管轮转
voicecraftali.New(voicecraftali.WithCredentialProvider(
    func(ctx context.Context) (voicecraftali.Credential, error) {
        key, expiresAt, err := fetchKey(ctx)
        return voicecraftali.Credential{Value: key, ExpiresAt: expiresAt}, err
    },
))

凭据来源的优先级:WithAPIKeyWithCredentialProvider 显式配置 > DASHSCOPE_API_KEY 环境变量 > ErrNoCredential。该变量未设置或为空字符串 一律视为未配置。

动态模式下本库负责缓存与并发去重:凭据在过期前被复用,并发请求只触发一次 provider 调用,且首个调用者取消不会连带其他等待者失败。 ExpiresAt 为零值表示永不过期。


多 API Key

一个 Client 绑定一套凭据与一套端点配置。进程内同时使用多个 API Key (多租户、多业务空间分别计费)时用 Registry:每个条目是一个逻辑名加一组 构造选项,因此可以各自配置业务空间、专属域名与自定义基址。

reg, err := voicecraftali.NewRegistry(
    // 共享选项应用到每个条目;条目自身的选项在其后应用,因而优先。
    []voicecraftali.Option{
        voicecraftali.WithUserAgent("my-svc/1.0"),
        voicecraftali.WithHTTPClient(sharedHTTPClient), // 全部条目复用同一个连接池
    },
    voicecraftali.RegistryEntry{
        Name:    "tenant-a",
        Options: []voicecraftali.Option{voicecraftali.WithAPIKey(keyA)},
    },
    voicecraftali.RegistryEntry{
        Name: "tenant-b",
        Options: []voicecraftali.Option{
            voicecraftali.WithAPIKey(keyB),
            voicecraftali.WithWorkspaceID("llm-b"),
            voicecraftali.WithDedicatedEndpoint(),
        },
    },
)
if err != nil {
    return err
}

client, err := reg.Client("tenant-b")
if err != nil {
    return err // 名字未注册时可用 errors.Is(err, voicecraftali.ErrUnknownClient) 判定
}

Registry 构造后不可变,可在多个 goroutine 间共享。Names() 返回按字典序 排序的条目名副本,用于启动期自检。

构造期会拒绝这些配置错误:条目列表为空、条目名为空、条目名重复、条目选项 校验失败。重名是错误而不是「后者覆盖前者」——被覆盖的名字会指向另一个 API Key,其调用被计入错误的账号。

条目必须显式配置凭据,不参与 DASHSCOPE_API_KEY 回退:漏配凭据的条目若静默 继承环境里的那一个 key,它的全部调用会被计入错误的账号。漏配的条目在 NewRegistry 返回 ErrNoCredential


地域与端点

voicecraftali.WithRegion(voicecraftali.RegionBeijing)    // 默认,dashscope.aliyuncs.com
voicecraftali.WithRegion(voicecraftali.RegionSingapore)  // dashscope-intl.aliyuncs.com

// 业务空间专属域名(阿里云推荐,性能与稳定性更好)
voicecraftali.WithWorkspaceID("llm-xxx")
voicecraftali.WithDedicatedEndpoint()  // → llm-xxx.cn-beijing.maas.aliyuncs.com

// 自定义基址(网关场景),接受 ws/wss/http/https
voicecraftali.WithBaseURL("wss://gw.example.com/aliyun")

连接复用

两套 WebSocket 合成 API 允许任务结束后在同一连接上开启下一个任务。 Pool 把复用规则封装起来,会话 API 保持不变:

pool := voicecraftali.NewPool(client,
    voicecraftali.WithPoolMaxIdle(16),
    voicecraftali.WithPoolIdleTimeout(45*time.Second),
)
defer pool.Close()

session, err := pool.NewSynthesisSession(ctx, params)
// ... 用法与 client.NewSynthesisSession 完全一致
_ = session.Close() // 干净结束的连接被交回池

池遵守服务端的三条规则:每个任务使用新的 task_idtask-failed 过的连接被丢弃; 空闲连接在服务端 60 秒断连阈值之前(默认 45 秒)被驱逐。

Qwen-TTS Realtime 的服务端在 session.finished 后主动关闭连接,用 Client 直接创建会话即可。


非实时 HTTP 合成

三套 HTTP 接口在音频交付形态上不一致,这是服务端的差异,本库如实反映:

API 非流式返回 流式返回
Qwen-Audio-TTS / CosyVoice 音频下载地址(24 小时有效) base64 分片
Qwen-TTS 音频下载地址(24 小时有效) base64 分片
MiniMax 音频字节(线上 hex 编码,本库已解码) hex 分片

非流式拿字节:

result, err := client.SynthesizeHTTP(ctx, params, "床前明月光")
if err != nil {
    return err
}
// 返回的是 URL,需要字节时再取一次;MiniMax 则直接有 result.Audio.Data
audio, err := result.Audio.Download(ctx, client)

流式边收边写:

result, err := client.StreamHTTP(ctx, params, text, func(audio []byte) error {
    _, err := file.Write(audio)
    return err
})

Audio.DownloadData 已有内容时直接返回它、不发请求,因此对三套接口可以写同一份代码。

MiniMax 的参数结构与阿里云自研模型不同:韵律与音频设置各自成组, 音量取值域是 (0, 10] 而非 [0, 100],音高是 [-12, 12] 的整数:

result, err := client.SynthesizeMiniMax(ctx, &voicecraftali.MiniMaxParams{
    Model: voicecraftali.ModelMiniMaxSpeech28HD,
    VoiceSetting: voicecraftali.MiniMaxVoiceSetting{
        VoiceID: "male-qn-qingse",
        Speed:   1.0,
        Emotion: voicecraftali.EmotionHappy,
    },
    AudioSetting: voicecraftali.MiniMaxAudioSetting{
        Format: voicecraftali.FormatMP3, SampleRate: 32000, Bitrate: 128000,
    },
}, "今天是不是很开心呀,当然了!")
// MiniMax 直接给字节
os.WriteFile("out.mp3", result.Audio.Data, 0o600)

能力与模型的对应关系

语音合成、声音复刻、声音设计能用的模型不是同一批。 接入前先看清楚是哪一层的 model:

例子 谁来设
管理接口路由名 voice-enrollmentqwen-voice-enrollmentqwen-voice-design 库内部硬编码,调用方接触不到
target_model cosyvoice-v3.5-plus 调用方,填各 Params 的 TargetModel
合成时的 model 同上 调用方,且必须与 target_model 逐字相同——官方原话「不能跨模型使用」

target_model 的取值域,复刻与设计是两个不同的集合:

模型 声音复刻 声音设计 地域
qwen-audio-3.0-tts-plus / -flash 复刻:北京、新加坡;设计:北京
cosyvoice-v3.5-plus / -flash 北京
cosyvoice-v3-plus 复刻:北京、新加坡;设计:北京
cosyvoice-v3-flash 北京
cosyvoice-v2 / v1 北京
qwen-audio-3.0-realtime-plus / -flash 北京
qwen3-tts-vc-*(3 个) 北京、新加坡
qwen3-tts-vd-*(3 个) 北京、新加坡
MiniMax/speech-*(4 个) 但不经 target_model 北京

qwen-audio-3.0-realtime-* 是实时语音对话模型,本库不覆盖它的合成 API, 收录这两个常量只为作复刻的绑定目标。

MiniMax 是个例外:它支持声音复刻,但走独立的 CloneMiniMaxVoice, 参数是自己的 Model 而不是 target_model,因此它不在 VoiceCloneTargetModels() 里,SupportsVoiceClone 对它返回 false。它的模型清单是 MiniMaxModels()

清单可以在代码里查:

voicecraftali.VoiceCloneTargetModels()  // 13 个,可作复刻 target_model
voicecraftali.VoiceDesignTargetModels() //  9 个,可作设计 target_model
voicecraftali.MiniMaxModels()           //  4 个,用于 MiniMax 的 Model 字段

if !voicecraftali.SupportsVoiceDesign(model) {
    // 例如 cosyvoice-v2 只能复刻,拿去设计会被服务端拒绝
}

这些清单是官方在册模型的快照,本库不用它们校验参数TargetModel 只校验非空, 官方新增的模型可以直接以字符串传入,不必等本库发版收录。要不要按清单前置拦截, 由调用方决定。返回 false 也不代表服务端一定拒绝——可能只是本库还没收录。


声音复刻

三套复刻接口的形态差异同样很大:

Qwen-Audio-TTS / CosyVoice Qwen-TTS MiniMax
样本音频 公网 URL 字节内联(Data URL),不超过 32 MiB 公网 URL
音色名 Prefix,服务端拼出 ID PreferredName VoiceID 完全自定义且全局唯一
审核状态 有(DEPLOYING/OK/UNDEPLOYED
试听音频 必须提供文本,按合成价额外计费

Qwen-TTS 的样本音频以字节内联提交,超过 32 MiB 时 CreateQwenVoice 在发出请求之前 返回 *ValidationError。这个上限是本库为内存放大设的保护线——音频要先 base64 编码 再序列化进请求体,峰值约为原始字节的 3.7 倍——不是服务端的业务限制, 服务端自己的限制(时长、采样率等)由服务端返回。

可用的 TargetModel能力与模型的对应关系VoiceCloneTargetModels()

// TargetModel 必须与后续合成时使用的模型一致,否则合成会失败
voiceID, err := client.CreateVoice(ctx, &voicecraftali.CreateVoiceParams{
    TargetModel: voicecraftali.ModelCosyVoiceV3Flash,
    Prefix:      "myvoice",
    URL:         "https://example.com/sample.wav",
})

// 音色创建后要过审核,状态变 OK 才能用于合成
detail, err := client.GetVoice(ctx, voiceID)
if detail.Status.Usable() {
    audio, err := client.Synthesize(ctx, &voicecraftali.SynthesisParams{
        Model: voicecraftali.ModelCosyVoiceV3Flash, // 与 TargetModel 一致
        Voice: voiceID,
    }, "这是用复刻音色合成的语音。")
}

Qwen-TTS 的复刻结果带 FallbackMode:为 true 表示样本音频质量不佳, 复刻是降级完成的,效果可能不理想——值得在接入流程里显式提示。

MiniMax 的复刻结果带 InputSensitive,表示输入音频是否命中风控。


声音设计

文字描述而不是样本音频生成音色,返回一段试听音频。

可用的 TargetModel 比复刻少,见 能力与模型的对应关系VoiceDesignTargetModels()——cosyvoice-v2cosyvoice-v1 与 MiniMax 都不能设计。

designed, err := client.DesignVoice(ctx, &voicecraftali.DesignVoiceParams{
    TargetModel: voicecraftali.ModelCosyVoiceV35Plus,
    VoicePrompt: "沉稳的中年男性,音色低沉浑厚",
    PreviewText: "各位听众朋友们大家好,欢迎收听本期节目",
    Prefix:      "announcer",
})
// 先听一耳朵再决定要不要落库
os.WriteFile("preview.wav", designed.Preview.Data, 0o600)
与声音复刻的关系

Qwen-Audio-TTS / CosyVoice 侧:设计与复刻产出的音色在服务端是同一批, 共用 ListVoices / GetVoice / DeleteVoice,用 Designed() 区分来源:

for _, v := range voices {
    if v.Designed() {
        log.Printf("%s ← 设计自「%s」", v.VoiceID, v.VoicePrompt)
    } else {
        log.Printf("%s ← 复刻", v.VoiceID)
    }
}

Qwen 侧:设计走 qwen-voice-design,复刻走 qwen-voice-enrollment, 是两批独立的音色,管理接口也各自独立。设计侧比复刻侧多一个详情接口

两侧约束的差异

设计参数的若干约束比复刻更严,照着复刻的规则写会被拒:

声音复刻 声音设计(CosyVoice 侧)
前缀字符集 数字/字母/下划线 数字/字母(无下划线
前缀长度 ≤ 16 ≤ 10
语言倾向 16 种 仅 zh / en
预览文本 必填,15–200 字符

Qwen 侧则更宽松:前缀允许下划线且 ≤16,语言倾向 10 种,预览文本 ≤1024, 预览音频还支持 8000 采样率与 opus 格式。


SSML 与 LaTeX

两项能力都只覆盖 CosyVoice 系列的五个模型(cosyvoice-v3.5-flash/plusv3-flash/plusv2),用 SupportsSSML(model) 判断。

SSML
ssml := "<speak>" +
    voicecraftali.EscapeSSMLText(userInput) +   // 外部文本必须先转义
    `<break time="500ms"/>` +
    `<sub alias="人工智能">AI</sub>` +
    "</speak>"

audio, err := client.Synthesize(ctx, &voicecraftali.SynthesisParams{
    Model:      voicecraftali.ModelCosyVoiceV3Flash,
    Voice:      "longanyang",
    EnableSSML: true,
}, ssml)

两条约束由本库在本地拦截,不会浪费一次往返:

  • 模型不支持时 EnableSSML: true 返回 ValidationError,且不建立连接
  • 启用后整个任务只能送一次文本,第二次 SendText 返回 ErrSSMLSingleSend (送第二段会让服务端报 Text request limit violated, expected 1. 并作废整个任务, 连已合成的音频一起丢)

音色维度还有限制——仅复刻音色与官方列表中标注支持 SSML 的系统音色可用, 这一层无法在本地判定,用错音色时由服务端返回错误。

EscapeSSMLText 转义 XML 的五个预定义实体。SSML 是 XML,文本里未转义的 &<> 会让整段标记解析失败——服务端返回的是参数错误,而不是把它们读出来。

LaTeX 公式朗读

不需要任何参数开关,把公式用 $...$$$...$$\(...\)\[...\] 包起来写进文本即可:

// 用反引号原样字符串,避免为反斜杠再转义一层
text := `求根公式:$x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$,请计算。`
audio, err := client.Synthesize(ctx, params, text)

仅支持中文朗读。LaTeX 文本不要EscapeSSMLText——反斜杠不是 XML 特殊字符。


错误处理

错误分五类具名类型,均支持 errors.Iserrors.As

类型 含义 携带信息
*ValidationError 本地参数校验失败 FieldReason
*TaskFailedError WebSocket 合成 API 的 task-failed TaskIDCodeMessage
*EventError Qwen-TTS Realtime 的 error 事件 EventIDCodeMessage
*HandshakeError WebSocket 握手失败 StatusCodeURL
*HTTPError 非实时 HTTP 接口失败 StatusCodeCodeMessageRequestIDRetryAfter

哨兵错误:ErrNoCredentialErrInvalidCredentialErrUnknownClientErrInvalidParamErrSessionClosedErrSessionFinishedErrTextTooLongErrUnsupportedOperationErrPoolClosedErrSSMLSingleSendErrStreamTruncated

服务端返回码收录为 Code 常量,同时覆盖两种命名风格(InvalidApiKeyinvalid_api_key),无需自行归一:

if code := voicecraftali.ErrorCode(err); code != "" {
    log.Printf("合成失败 %s:%s", code, voicecraftali.CodeDescription(code))
}

if voicecraftali.Retryable(err) {
    // 限流、服务端内部错误、模型不可用、超时——退避后重试
}

Retryable 只做判定,退避策略、抖动与次数上限由调用方决定。判定为可重试的情形是 限流、服务端内部错误、模型不可用、超时类返回码,握手失败且状态码为 429 或 5xx, 以及 ErrStreamTruncated——SSE 流在结束标记之前断开的成因是中间代理超时或 服务端异常收尾,属于瞬时故障。

服务端在限流响应里给出 Retry-After 时,该值解析后放在 *HTTPError.RetryAfter, 让退避有依据而不必拍一个数字(秒数与 HTTP-date 两种形式都解析;服务端没给时为 0):

if voicecraftali.Retryable(err) {
    wait := backoff() // 调用方自己的退避策略
    if httpErr, ok := errors.AsType[*voicecraftali.HTTPError](err); ok && httpErr.RetryAfter > 0 {
        wait = httpErr.RetryAfter // 服务端给了就听服务端的
    }
    time.Sleep(wait)
}

本库不依据该字段自行重试:WebSocket 会话重试要回滚已经交给调用方的音频, HTTP 合成是计费操作,两者都只有调用方知道该怎么做。


并发与资源

Client 构造后不可变,可在多个 goroutine 间共享。

Session 的并发契约:

  • 一个 goroutine 写(SendText / Commit / Finish / Cancel)与另一个 goroutine 读(Read / ReadContext / Stream)可以并发进行
  • 多个 goroutine 并发写是安全的,写入被串行化
  • Close 幂等,可由任意 goroutine 在任意时刻调用;返回后保证接收 goroutine 已退出、 连接已释放、阻塞在读取上的调用方已被解除阻塞

串行化只保证安全,不提升吞吐——一个会话就是一条 WebSocket 连接。需要并发合成请创建多个会话。

调用方必须持续读取音频:音频经有界缓冲交付(默认 64 帧,WithAudioBuffer 可调), 缓冲写满时接收阻塞形成反压而不丢帧;长时间不读会把反压传导到服务端并触发其空闲断连。

超时
控制项 作用范围
传入各方法的 ctx 单次调用的整体时长,WebSocket 与 HTTP 两条路径都生效
WithHandshakeTimeout WebSocket 握手,默认 10 秒
WithReadIdleTimeout WebSocket 单次读取的空闲上限,默认 60 秒,每次成功读取后续期
WithWriteTimeout WebSocket 单次写入,默认 10 秒

WithHTTPClient 传入的 http.Client 贡献的是 Transport 上的代理、TLS 与拨号配置, 其 Timeout 字段被忽略——该字段覆盖「连接建立到响应体读完」的全过程, 会把一条正常的 SSE 音频流拦腰截断。HTTP 侧的超时请用 ctx 表达:

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
result, err := client.SynthesizeHTTP(ctx, params, text)

传入的 http.Client 实例不会被本库修改(Timeout 的清零只作用于内部副本), 因此可以安全地把进程内共享的那一个传进来。


合成进度与时间戳

session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
    Model:                voicecraftali.ModelCosyVoiceV3Flash,
    Voice:                "longanyang",
    WordTimestampEnabled: true,
}, voicecraftali.WithProgress(func(p voicecraftali.Progress) {
    if p.Type == "sentence-end" && p.Sentence != nil {
        for _, w := range p.Sentence.Words {
            log.Printf("%q %d-%dms", w.Text, w.BeginTime, w.EndTime)
        }
    }
}))

回调在接收 goroutine 上同步执行,与音频投递共用同一个 goroutine: 回调阻塞会同时挡住音频投递,因此其中不要做耗时操作。

Session.Usage() 返回服务端给出的计费信息(字符数或 Token 数), 应在音频流结束之后读取以拿到最终值。


音色与模型

音色以字符串传入。数量稳定的两组系统音色已收录为常量: Qwen-Audio-TTS 14 个(VoiceLongAnHuan 等)、Qwen-TTS Realtime 48 个(VoiceCherry 等)。 CosyVoice 的系统音色有 200 余个且随模型版本增补,直接传字符串即可; 声音复刻与声音设计生成的专属音色同样以字符串传入。

音色与模型是绑定的:用错模型对应的音色,服务端返回 CodeBadRequestVoiceNotFoundCodeInvalidValue。完整音色列表见官方文档:


依赖

依赖 用途
github.com/gtkit/json/v2 JSON 编解码
github.com/gtkit/httpc 非实时 HTTP 请求(连接池、响应体上限)
github.com/gtkit/ssex 非实时流式模式的上游 SSE 解码
github.com/gorilla/websocket WebSocket 客户端
golang.org/x/sync singleflight 凭据刷新去重

task_idevent_id 用标准库 uuid 包生成 UUID v4(其随机源是 crypto/rand), 不引入额外依赖。 本库不打日志,所有结果通过返回值上抛,由调用方决定如何记录。


官方文档


许可证

MIT

Documentation

Overview

Package voicecraftali 提供阿里云百炼(Model Studio)语音合成的 Go 客户端, 覆盖三套实时 WebSocket API、三套非实时 HTTP API、声音复刻与声音设计。

实时合成(WebSocket)

阿里云对外暴露三套形态不同的 WebSocket API,本包把它们收敛到同一个 Session 类型上:

| API                        | 端点                  | 模型                                   |
|---------------------------|----------------------|----------------------------------------|
| CosyVoice / Qwen-Audio-TTS | /api-ws/v1/inference | qwen-audio-3.0-tts-*、cosyvoice-v1~v3.5 |
| Sambert                    | /api-ws/v1/inference | sambert-*-v1(单向流式)                 |
| Qwen-TTS Realtime          | /api-ws/v1/realtime  | qwen3-tts-*-realtime、qwen-tts-realtime |

分别通过 Client.NewSynthesisSession、Client.NewSambertSession 与 Client.NewRealtimeSession 创建会话。三者返回的都是 *Session, 操作集合一致,切换 API 不需要改动读写代码。各 API 确实存在的能力差异以 ErrUnsupportedOperation 显式暴露,完整矩阵见 Session 的文档。

会话的写侧动作分终结与非终结两类。非终结动作调用后会话继续可用: SendText 送文本、Commit 提交缓冲区(Realtime)、Flush 强制合成已送入的文本 (CosyVoice / Qwen-Audio-TTS)、CancelResponse 取消当前响应(Realtime)。 终结动作调用后不能再送文本,但仍应读到音频流结束:Finish 正常结束、 Cancel 中断当前任务(CosyVoice / Qwen-Audio-TTS)。

快速开始

流式合成——边送文本边取音频:

client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
if err != nil {
    return err
}

session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
    Model:  voicecraftali.ModelCosyVoiceV3Flash,
    Voice:  "longanyang",
    Format: voicecraftali.FormatMP3,
})
if err != nil {
    return err
}
defer session.Close()

go func() {
    _ = session.SendText(ctx, "床前明月光,疑是地上霜。")
    _ = session.SendText(ctx, "举头望明月,低头思故乡。")
    _ = session.Finish(ctx)
}()

err = session.Stream(ctx, func(audio []byte) error {
    _, err := file.Write(audio)
    return err
})

一次性合成——短文本直接拿到完整音频:

audio, err := client.Synthesize(ctx, params, "床前明月光,疑是地上霜。")

鉴权

阿里云百炼使用 API Key 鉴权,凭据在 WebSocket 握手阶段校验。 静态密钥用 WithAPIKey;短期密钥、STS 临时凭证或密钥托管轮转的场景用 WithCredentialProvider,本包负责缓存与并发去重,凭据在过期前被复用, 并发请求只触发一次刷新。

两者都不配时,New 回退到 DASHSCOPE_API_KEY 环境变量——这是阿里云百炼文档 统一推荐的变量名。优先级为:显式选项 > 环境变量 > ErrNoCredential。

client, err := voicecraftali.New() // 从 DASHSCOPE_API_KEY 取凭据

北京地域与新加坡地域的 API Key 不通用,需与 WithRegion 匹配。

多 API Key

一个 Client 绑定一套凭据与一套端点配置。进程内要同时使用多个 API Key (多租户、多业务空间分别计费)时用 Registry:每个条目是一个逻辑名加一组 构造选项,因此可以各自配置业务空间、专属域名与自定义基址。

reg, err := voicecraftali.NewRegistry(
    []voicecraftali.Option{voicecraftali.WithUserAgent("svc/1.0")},
    voicecraftali.RegistryEntry{
        Name:    "tenant-a",
        Options: []voicecraftali.Option{voicecraftali.WithAPIKey(keyA)},
    },
    voicecraftali.RegistryEntry{
        Name: "tenant-b",
        Options: []voicecraftali.Option{
            voicecraftali.WithAPIKey(keyB),
            voicecraftali.WithWorkspaceID("llm-b"),
            voicecraftali.WithDedicatedEndpoint(),
        },
    },
)
if err != nil {
    return err
}
client, err := reg.Client("tenant-b")

注册表条目必须显式配置凭据,不参与 DASHSCOPE_API_KEY 回退:漏配的条目若 静默继承环境里的那一个 key,它的调用会被计入错误的账号。

并发与资源

Client 构造后不可变,可在多个 goroutine 间共享。

Session 支持「一个 goroutine 写、另一个 goroutine 读」并发使用; 多个 goroutine 并发写会被串行化。Close 幂等,返回后保证后台 goroutine 已退出、连接已释放。详见 Session 的文档。

音频通过有界缓冲交付,写满时接收阻塞形成反压而不丢帧,因此调用方必须 持续读取——长时间不读会把反压传导到服务端并触发其空闲断连。

连接复用

两套 WebSocket 合成 API 允许在一条连接上按顺序执行多个任务。Pool 把复用 规则(任务结束后换新 task_id、失败过的连接不复用、赶在服务端 60 秒空闲 阈值之前驱逐)封装起来,会话 API 保持不变:

pool := voicecraftali.NewPool(client)
defer pool.Close()
session, err := pool.NewSynthesisSession(ctx, params)

Qwen-TTS Realtime 的服务端在会话结束后主动关闭连接,用 Client 直接创建即可。

非实时合成(HTTP)

三套 HTTP 接口各有非流式与 SSE 流式两种模式,仅在北京地域可用:

| API                        | 非流式             | 流式             |
|---------------------------|-------------------|------------------|
| Qwen-Audio-TTS / CosyVoice | SynthesizeHTTP    | StreamHTTP       |
| Qwen-TTS                   | SynthesizeQwenTTS | StreamQwenTTS    |
| MiniMax                    | SynthesizeMiniMax | StreamMiniMax    |

音频交付形态因接口而异,这是服务端的差异:前两套的非流式模式返回有效期 24 小时的下载地址,MiniMax 与所有流式模式直接返回音频字节。 AudioRef.Download 屏蔽了这个差异——已有字节时直接返回,不发请求:

result, err := client.SynthesizeHTTP(ctx, params, "床前明月光")
if err != nil {
    return err
}
audio, err := result.Audio.Download(ctx, client)

声音复刻与音色管理

三套复刻接口的操作集合不同:

| 模型家族                    | 创建               | 列表            | 详情     | 更新        | 删除              |
|---------------------------|-------------------|----------------|---------|------------|------------------|
| Qwen-Audio-TTS / CosyVoice | CreateVoice       | ListVoices     | GetVoice | UpdateVoice | DeleteVoice      |
| Qwen-TTS                   | CreateQwenVoice   | ListQwenVoices | —       | —          | DeleteQwenVoice  |
| MiniMax                    | CloneMiniMaxVoice | —              | —       | —          | —                |

复刻时指定的 TargetModel 必须与后续合成时使用的模型一致,否则合成会失败。 Qwen-Audio-TTS / CosyVoice 的音色创建后要经过审核, VoiceStatus.Usable 为 true 才能用于合成。

能力与模型的对应关系

语音合成、声音复刻、声音设计能用的模型不是同一批。target_model 的取值域 复刻是 13 个、设计是 9 个,差集包括:cosyvoice-v2、cosyvoice-v1 与 Qwen-Audio-Realtime 只能复刻;Qwen-TTS 侧复刻只收 qwen3-tts-vc-*、 设计只收 qwen3-tts-vd-*,两组互不通用。

voicecraftali.VoiceCloneTargetModels()  // 可作复刻 target_model 的 13 个模型
voicecraftali.VoiceDesignTargetModels() // 可作设计 target_model 的 9 个模型
voicecraftali.MiniMaxModels()           // MiniMax 的 4 个模型

if !voicecraftali.SupportsVoiceDesign(model) { /* 该模型不能用于声音设计 */ }

MiniMax 是例外:它支持声音复刻,但走 CloneMiniMaxVoice、用自己的 Model 字段 而不是 target_model,因此不在 VoiceCloneTargetModels 里, SupportsVoiceClone 对它返回 false。

这些清单是官方在册模型的快照,本库不用它们校验参数——TargetModel 只校验非空, 官方新增的模型可以直接传入,不必等本库收录。

声音设计

用文字描述而不是样本音频生成音色,并返回一段试听音频:

designed, err := client.DesignVoice(ctx, &voicecraftali.DesignVoiceParams{
    TargetModel: voicecraftali.ModelCosyVoiceV35Plus,
    VoicePrompt: "沉稳的中年男性,音色低沉浑厚",
    PreviewText: "各位听众朋友们大家好,欢迎收听本期节目",
    Prefix:      "announcer",
})

Qwen-Audio-TTS / CosyVoice 侧的设计音色与复刻音色在服务端是同一批, 共用 ListVoices、GetVoice 与 DeleteVoice,用 VoiceSummary.Designed 与 VoiceDetail.Designed 区分来源。Qwen 侧的设计走独立的模型, 因此有独立的 ListDesignedQwenVoices、GetDesignedQwenVoice 与 DeleteDesignedQwenVoice。

SSML 与 LaTeX

两项能力都只覆盖 CosyVoice 系列的五个模型,用 SupportsSSML 判断。

SSML 需要把 EnableSSML 设为 true,且拼接时外部文本要先过 EscapeSSMLText—— SSML 是 XML,未转义的 & 或 < 会让整段标记解析失败。启用 SSML 后整个任务 只允许送一次文本,第二次 SendText 返回 ErrSSMLSingleSend。

LaTeX 公式朗读不需要任何参数:把公式用 $...$ 包起来写进文本即可, 仅支持中文。LaTeX 文本不要过 EscapeSSMLText——反斜杠不是 XML 特殊字符。

错误处理

错误分五类具名类型:ValidationError(本地参数校验)、TaskFailedError (两套 WebSocket 合成 API 的 task-failed)、EventError(Qwen-TTS Realtime 的 error 事件)、HandshakeError(WebSocket 握手失败)、HTTPError (非实时 HTTP 接口失败),均支持 errors.Is 与 errors.As。

服务端返回码收录于 codes.go,CodeDescription 给出中文说明, Retryable 判断该错误是否值得重试:

if err != nil {
    if code := voicecraftali.ErrorCode(err); code != "" {
        log.Printf("合成失败 %s: %s", code, voicecraftali.CodeDescription(code))
    }
    if voicecraftali.Retryable(err) {
        // 限流、服务端内部错误、模型不可用、超时——退避后重试
    }
}

重试策略(退避、抖动、次数上限)由调用方决定,本包只提供判定。

Index

Examples

Constants

View Source
const (
	LanguageAuto       = "Auto"
	LanguageChinese    = "Chinese"
	LanguageEnglish    = "English"
	LanguageGerman     = "German"
	LanguageItalian    = "Italian"
	LanguagePortuguese = "Portuguese"
	LanguageSpanish    = "Spanish"
	LanguageJapanese   = "Japanese"
	LanguageKorean     = "Korean"
	LanguageFrench     = "French"
	LanguageRussian    = "Russian"
)

Qwen-TTS Realtime 支持的 language_type 取值。

View Source
const (
	// ModelMiniMaxSpeech28HD MiniMax/speech-2.8-hd,效果优先。
	ModelMiniMaxSpeech28HD = "MiniMax/speech-2.8-hd"
	// ModelMiniMaxSpeech02HD MiniMax/speech-02-hd。
	ModelMiniMaxSpeech02HD = "MiniMax/speech-02-hd"
	// ModelMiniMaxSpeech28Turbo MiniMax/speech-2.8-turbo,延迟优先。
	ModelMiniMaxSpeech28Turbo = "MiniMax/speech-2.8-turbo"
	// ModelMiniMaxSpeech02Turbo MiniMax/speech-02-turbo。
	ModelMiniMaxSpeech02Turbo = "MiniMax/speech-02-turbo"
)

MiniMax 非实时 HTTP 合成的模型。

View Source
const (
	EmotionHappy     = "happy"     // 高兴
	EmotionSad       = "sad"       // 悲伤
	EmotionAngry     = "angry"     // 愤怒
	EmotionFearful   = "fearful"   // 害怕
	EmotionDisgusted = "disgusted" // 厌恶
	EmotionSurprised = "surprised" // 惊讶
	EmotionCalm      = "calm"      // 中性
	// EmotionWhisper 低语。speech-2.8-hd 与 speech-2.8-turbo 不支持。
	EmotionWhisper = "whisper"
)

MiniMax 支持的情感。

View Source
const (
	// ModelQwen3TTSFlash qwen3-tts-flash 稳定版。
	ModelQwen3TTSFlash = "qwen3-tts-flash"
	// ModelQwen3TTSInstructFlash qwen3-tts-instruct-flash 稳定版,支持指令控制。
	ModelQwen3TTSInstructFlash = "qwen3-tts-instruct-flash"
	// ModelQwen3TTSVC qwen3-tts-vc 快照版,用于声音复刻音色。
	ModelQwen3TTSVC = "qwen3-tts-vc-2026-01-22"
	// ModelQwen3TTSVD qwen3-tts-vd 快照版,用于声音设计音色。
	ModelQwen3TTSVD = "qwen3-tts-vd-2026-01-26"
	// ModelQwenTTS qwen-tts 稳定版(旧版,按 Token 计费)。
	ModelQwenTTS = "qwen-tts"
	// ModelQwenTTSLatest qwen-tts-latest(旧版,按 Token 计费)。
	ModelQwenTTSLatest = "qwen-tts-latest"
)

Qwen-TTS 非实时 HTTP 合成的模型。

与 WebSocket 侧按模型名后缀区分:带 -realtime 的走 WebSocket, 不带的走本 HTTP 接口。

View Source
const (
	// ModelQwenAudioTTSPlus qwen-audio-3.0-tts-plus,效果优先。
	ModelQwenAudioTTSPlus = "qwen-audio-3.0-tts-plus"
	// ModelQwenAudioTTSFlash qwen-audio-3.0-tts-flash,延迟优先。
	ModelQwenAudioTTSFlash = "qwen-audio-3.0-tts-flash"

	// ModelCosyVoiceV35Plus cosyvoice-v3.5-plus(仅北京地域)。
	ModelCosyVoiceV35Plus = "cosyvoice-v3.5-plus"
	// ModelCosyVoiceV35Flash cosyvoice-v3.5-flash(仅北京地域)。
	ModelCosyVoiceV35Flash = "cosyvoice-v3.5-flash"
	// ModelCosyVoiceV3Plus cosyvoice-v3-plus。
	ModelCosyVoiceV3Plus = "cosyvoice-v3-plus"
	// ModelCosyVoiceV3Flash cosyvoice-v3-flash。
	ModelCosyVoiceV3Flash = "cosyvoice-v3-flash"
	// ModelCosyVoiceV2 cosyvoice-v2(仅北京地域)。
	ModelCosyVoiceV2 = "cosyvoice-v2"
	// ModelCosyVoiceV1 cosyvoice-v1(仅北京地域)。不支持 opus 格式、bit_rate 与 seed 参数。
	ModelCosyVoiceV1 = "cosyvoice-v1"
)

CosyVoice / Qwen-Audio-TTS(WebSocket 双向流式)的模型。

这两个系列可在会话中多次 SendText 增量送入文本。

View Source
const (
	// ModelQwenAudioRealtimePlus qwen-audio-3.0-realtime-plus(仅北京地域)。
	ModelQwenAudioRealtimePlus = "qwen-audio-3.0-realtime-plus"
	// ModelQwenAudioRealtimeFlash qwen-audio-3.0-realtime-flash(仅北京地域)。
	ModelQwenAudioRealtimeFlash = "qwen-audio-3.0-realtime-flash"
)

Qwen-Audio-Realtime 模型。

本库不覆盖该系列的合成 API——它是实时语音对话模型,不是 TTS 接口。 收录这两个常量只为一个用途:它们是官方在册的声音复刻 target_model, 可填入 CreateVoiceParams.TargetModel 把音色绑定到该系列。 绑定后的合成需自行调用对话 API,本库不提供。

View Source
const (
	// ModelQwen3TTSInstructFlashRealtime qwen3-tts-instruct-flash-realtime 稳定版,
	// 支持 Instructions 指令控制。
	ModelQwen3TTSInstructFlashRealtime = "qwen3-tts-instruct-flash-realtime"
	// ModelQwen3TTSFlashRealtime qwen3-tts-flash-realtime 稳定版。
	ModelQwen3TTSFlashRealtime = "qwen3-tts-flash-realtime"
	// ModelQwen3TTSVCRealtime qwen3-tts-vc-realtime 最新快照版,用于声音复刻(Qwen)音色。
	ModelQwen3TTSVCRealtime = "qwen3-tts-vc-realtime-2026-01-15"
	// ModelQwen3TTSVCRealtimePrev qwen3-tts-vc-realtime 上一个快照版。
	// 官方仍在册,供已绑定该版本的存量音色继续使用;新音色用
	// ModelQwen3TTSVCRealtime。
	ModelQwen3TTSVCRealtimePrev = "qwen3-tts-vc-realtime-2025-11-27"
	// ModelQwen3TTSVDRealtime qwen3-tts-vd-realtime 最新快照版,用于声音设计(Qwen)音色。
	ModelQwen3TTSVDRealtime = "qwen3-tts-vd-realtime-2026-01-15"
	// ModelQwen3TTSVDRealtimePrev qwen3-tts-vd-realtime 上一个快照版。
	// 官方仍在册,供已绑定该版本的存量音色继续使用;新音色用
	// ModelQwen3TTSVDRealtime。
	ModelQwen3TTSVDRealtimePrev = "qwen3-tts-vd-realtime-2025-12-16"
	// ModelQwenTTSRealtime qwen-tts-realtime 稳定版。
	// 该系列只支持 pcm 格式与 24000 采样率,且不支持语速、音量、语调与码率参数。
	ModelQwenTTSRealtime = "qwen-tts-realtime"
)

Qwen-TTS Realtime(WebSocket 事件流)的模型。

View Source
const (
	// ModelSambertZhichu sambert-zhichu-v1。
	ModelSambertZhichu = "sambert-zhichu-v1"
	// ModelSambertZhinan sambert-zhinan-v1。
	ModelSambertZhinan = "sambert-zhinan-v1"
	// ModelSambertZhiqi sambert-zhiqi-v1。
	ModelSambertZhiqi = "sambert-zhiqi-v1"
	// ModelSambertZhide sambert-zhide-v1。
	ModelSambertZhide = "sambert-zhide-v1"
	// ModelSambertZhijia sambert-zhijia-v1。
	ModelSambertZhijia = "sambert-zhijia-v1"
	// ModelSambertZhiru sambert-zhiru-v1。
	ModelSambertZhiru = "sambert-zhiru-v1"
)

Sambert 模型(WebSocket 单向流式)。

Sambert 是早期语音合成模型,走单向流式:全文在建立会话时一次性提交, 不支持增量 SendText。新项目建议优先选用 CosyVoice 或 Qwen-Audio-TTS。 完整模型列表见官方 Sambert 文档。

View Source
const (
	VoiceLongAnLingXin = "longanlingxin"   // 龙安灵心,知心温暖音,女
	VoiceLongAnLuFeng  = "longanlufeng"    // 龙安鲁风,明亮开朗音,男
	VoiceLongAnFengYue = "longanfengyue"   // 龙安风悦,自然亲切音,女
	VoiceLongAnYuanFei = "longanyuanfei"   // 龙安元妃,高傲妃子音,女
	VoiceLongAnLingXi  = "longanlingxi"    // 龙安灵希,可爱甜美音,女
	VoiceLongAnXiaoXin = "longanxiaoxin"   // 龙安小昕,亲切活泼音,女
	VoiceLongAnHuan    = "longanhuan_v3.6" // 龙安欢,女
	VoiceLongJieLiDou  = "longjielidou_v3.6"
	VoiceLongPaoPao    = "longpaopao_v3.6"
	VoiceLongHuoHuo    = "longhuohuo_v3.6"
	VoiceLongChuanShu  = "longchuanshu_v3.6"
	VoiceLoongMary     = "loongmary"
	VoiceLoongEva      = "loongeva_v3.6"
	VoiceLoongJohn     = "loongjohn"
)

Qwen-Audio-TTS 系统音色(配合 ModelQwenAudioTTSPlus / ModelQwenAudioTTSFlash)。

View Source
const (
	VoiceCherry     = "Cherry"      // 芊悦,阳光积极、亲切自然,女
	VoiceSerena     = "Serena"      // 苏瑶,温柔,女
	VoiceEthan      = "Ethan"       // 晨煦,阳光温暖有活力,男
	VoiceChelsie    = "Chelsie"     // 千雪,二次元虚拟女友,女
	VoiceMomo       = "Momo"        // 茉兔,撒娇搞怪,女
	VoiceVivian     = "Vivian"      // 十三,拽拽的小暴躁,女
	VoiceMoon       = "Moon"        // 月白,率性帅气,男
	VoiceMaia       = "Maia"        // 四月,知性温柔,女
	VoiceKai        = "Kai"         // 凯,男
	VoiceNofish     = "Nofish"      // 不吃鱼,不会翘舌音的设计师,男
	VoiceBella      = "Bella"       // 萌宝,小萝莉,女
	VoiceJennifer   = "Jennifer"    // 詹妮弗,电影质感美语女声
	VoiceRyan       = "Ryan"        // 甜茶,戏感张力,男
	VoiceKaterina   = "Katerina"    // 卡捷琳娜,御姐音色,女
	VoiceAiden      = "Aiden"       // 艾登,美语大男孩
	VoiceEldricSage = "Eldric Sage" // 沧明子,沉稳睿智的老者,男
	VoiceMia        = "Mia"         // 乖小妹,女
	VoiceMochi      = "Mochi"       // 沙小弥,聪明伶俐的小大人,男
	VoiceBellona    = "Bellona"     // 燕铮莺,声音洪亮吐字清晰,女
	VoiceVincent    = "Vincent"     // 田叔,沙哑烟嗓,男
	VoiceBunny      = "Bunny"       // 萌小姬,小萝莉,女
	VoiceNeil       = "Neil"        // 阿闻,新闻主持人,男
	VoiceElias      = "Elias"       // 墨讲师,学科严谨,女
	VoiceArthur     = "Arthur"      // 徐大爷,质朴嗓音,男
	VoiceNini       = "Nini"        // 邻家妹妹
	VoiceSeren      = "Seren"       // 小婉
	VoicePip        = "Pip"         // 顽屁小孩
	VoiceStella     = "Stella"      // 少女阿月
	VoiceBodega     = "Bodega"      // 博德加
	VoiceSonrisa    = "Sonrisa"     // 索尼莎
	VoiceAlek       = "Alek"        // 阿列克
	VoiceDolce      = "Dolce"       // 多尔切
	VoiceSohee      = "Sohee"       // 素熙
	VoiceOnoAnna    = "Ono Anna"    // 小野杏
	VoiceLenn       = "Lenn"        // 莱恩
	VoiceEmilien    = "Emilien"     // 埃米尔安
	VoiceAndre      = "Andre"       // 安德雷
	VoiceRadioGol   = "Radio Gol"   // 拉迪奥·戈尔
)

Qwen-TTS Realtime 系统音色(配合 ModelQwen3TTS*Realtime / ModelQwenTTSRealtime)。

各音色支持的具体模型版本不同,详见官方 Qwen-TTS 音色列表文档。

View Source
const (
	VoiceJada   = "Jada"   // 上海-阿珍
	VoiceDylan  = "Dylan"  // 北京-晓东
	VoiceLi     = "Li"     // 南京-老李
	VoiceMarcus = "Marcus" // 陕西-秦川
	VoiceRoy    = "Roy"    // 闽南-阿杰
	VoicePeter  = "Peter"  // 天津-李彼得
	VoiceSunny  = "Sunny"  // 四川-晴儿
	VoiceEric   = "Eric"   // 四川-程川
	VoiceRocky  = "Rocky"  // 粤语-阿强
	VoiceKiki   = "Kiki"   // 粤语-阿清
)

Qwen-TTS Realtime 方言音色。

View Source
const (
	// SensitiveNone 正常,未命中风控。
	SensitiveNone = 0
	// SensitiveSevere 严重违规。
	SensitiveSevere = 1
	// SensitivePorn 色情。
	SensitivePorn = 2
)

MiniMax 输入音频命中风控的类型。

View Source
const Version = "v1.2.0"

Version 是本包的当前版本号,与 git 附注标签保持一致。

发版走 make release-patch / make release-minor:脚本会原地自增下面这行的版本号、 提交并据此打标签,因此这一行的形状——Version 常量赋值为带 v 前缀的三段版本号—— 不能改动,本注释内也不得出现版本号字面量(脚本取文件里第一个匹配到的版本号)。

Variables

View Source
var (
	// ErrNoCredential 表示构造时找不到可用凭据。
	//
	// New 的凭据来源优先级为:WithAPIKey 或 WithCredentialProvider 显式配置 >
	// DASHSCOPE_API_KEY 环境变量 > 本错误。NewRegistry 的条目不参与环境变量
	// 回退,未显式配置凭据即返回本错误。
	ErrNoCredential = errors.New("voicecraftali: no credential configured")

	// ErrInvalidCredential 表示凭据提供者返回了空凭据。空凭据无法用于握手,
	// 因此不会被缓存,也不会被送往服务端。
	ErrInvalidCredential = errors.New("voicecraftali: credential provider returned an empty credential")

	// ErrUnknownClient 表示 Registry 中不存在该名字的条目。
	//
	// 独立成一个哨兵错误是为了让多租户调用方能把「未知条目名」与配置错误分开
	// 处理——前者通常对应一个客户端错误,后者对应服务端配置问题。
	ErrUnknownClient = errors.New("voicecraftali: unknown client name")

	// ErrInvalidParam 表示参数校验失败。所有 *ValidationError 都能被
	// errors.Is(err, ErrInvalidParam) 命中,便于调用方在不关心具体字段时统一分支。
	ErrInvalidParam = errors.New("voicecraftali: invalid parameter")

	// ErrSessionClosed 表示会话已关闭,不能再执行写操作。
	ErrSessionClosed = errors.New("voicecraftali: session closed")

	// ErrSessionFinished 表示会话已调用 Finish,不能再送入文本。
	// 已 Finish 的会话仍可继续读取服务端返回的剩余音频。
	ErrSessionFinished = errors.New("voicecraftali: session already finished")

	// ErrTextTooLong 表示待合成文本超过协议允许的单次长度上限。
	ErrTextTooLong = errors.New("voicecraftali: text exceeds length limit")

	// ErrUnsupportedOperation 表示当前协议或模型不支持该操作,
	// 例如在 WebSocket 合成 API 上调用 Commit、在 Sambert 或
	// Qwen-TTS Realtime 上调用 Cancel、在 Sambert 上调用 SendText。
	ErrUnsupportedOperation = errors.New("voicecraftali: operation not supported by this protocol")

	// ErrPoolClosed 表示连接池已关闭,不能再从中创建会话。
	ErrPoolClosed = errors.New("voicecraftali: pool closed")

	// ErrSSMLSingleSend 表示会话启用了 SSML,而 SSML 模式下整个任务
	// 只允许送入一次文本。
	ErrSSMLSingleSend = errors.New("voicecraftali: SSML mode allows only one text submission")
)

哨兵错误。调用方用 errors.Is 判断错误类别。

View Source
var ErrStreamTruncated = errors.New("voicecraftali: sse stream ended before the completion marker")

ErrStreamTruncated 表示上游 SSE 流在给出结束标记之前就结束了。

这通常是中间代理超时或服务端异常收尾造成的。此时已收到的音频是残缺的, 本库把它作为错误上报而不是当成正常结束——静默返回半截音频会被调用方 当成完整结果写进文件,比直接失败更难排查。

Functions

func CodeDescription

func CodeDescription(code Code) string

CodeDescription 返回返回码的中文说明。

未收录的码(含空字符串)返回空字符串——阿里云会随文档更新新增返回码, 让未知码表现为「没有说明」而不是 panic 或错误的说明。

func DataURLMIME

func DataURLMIME(dataURL string) string

DataURLMIME 从 Data URL 中取出 MIME 类型,用于校验或排查。 不是 Data URL 时返回空字符串。

func EscapeSSMLText

func EscapeSSMLText(s string) string

EscapeSSMLText 转义一段要嵌进 SSML 的普通文本。

SSML 是 XML:文本里未转义的 &、<、> 会让整段标记解析失败,服务端返回 参数错误而不是把它们读出来。拼接 SSML 时,凡是来自外部的文本都应先过一遍 本函数:

text := "Q&A 环节 <请注意>"
ssml := "<speak>" + voicecraftali.EscapeSSMLText(text) + "</speak>"
// <speak>Q&amp;A 环节 &lt;请注意&gt;</speak>

只转义文本内容,不校验 SSML 结构——标签的正确性由调用方保证。

LaTeX 公式不要用本函数处理:反斜杠不是 XML 特殊字符,无需转义; 需要留意的是 Go 字符串字面量里 \ 要写成 \\(如 LaTeX 的 \frac 写作 "\\frac"), 或者直接用反引号原样字符串。

Example

ExampleEscapeSSMLText 展示用 SSML 精细控制发音。

SSML 是 XML:来自外部的文本必须先转义,否则 & 或 < 会让整段标记解析失败。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	userInput := `Q&A 环节 <请注意>` // 来自外部,含 XML 特殊字符

	ssml := "<speak>" +
		voicecraftali.EscapeSSMLText(userInput) +
		`<break time="500ms"/>` +
		`<sub alias="人工智能">AI</sub>` +
		`<say-as interpret-as="telephone">10086</say-as>` +
		"</speak>"

	// SSML 仅 CosyVoice 系列的部分模型支持,可先用 SupportsSSML 判断。
	model := voicecraftali.ModelCosyVoiceV3Flash
	if !voicecraftali.SupportsSSML(model) {
		log.Fatalf("模型 %s 不支持 SSML", model)
	}

	audio, err := client.Synthesize(context.Background(), &voicecraftali.SynthesisParams{
		Model:      model,
		Voice:      "longanyang",
		EnableSSML: true, // 启用后整个任务只能送一次文本
	}, ssml)
	if err != nil {
		log.Fatal(err)
	}
	_ = audio
}

func MiniMaxModels added in v1.1.0

func MiniMaxModels() []string

MiniMaxModels 返回 MiniMax 系列的全部模型名。

这些取值用于 MiniMaxParams.Model(合成)与 MiniMaxCloneParams.Model(声音复刻), **不用于任何 TargetModel 字段**——MiniMax 的复刻不经过 target_model。

与这两处参数校验用的是同一份清单,因此本函数返回的每个取值都能通过校验。

返回的是副本,调用方修改它不会影响本库。

func Retryable

func Retryable(err error) bool

Retryable 判断错误是否值得重试。

判定为可重试的情形:

  • 服务端返回限流、内部错误、模型不可用、超时类返回码
  • 握手失败且 HTTP 状态码为 429 或 5xx
  • SSE 流在结束标记之前断开(ErrStreamTruncated)

判定为不可重试的情形:参数错误、鉴权错误、欠费、资源不存在, 以及 nil、未收录返回码与本库之外的错误。

本库不内置重试循环:退避策略、抖动与次数上限属于调用方的策略选择, 可与 github.com/gtkit/tools/retry 之类的组件配合:

for attempt := range 3 {
    audio, err := client.Synthesize(ctx, params, text)
    if err == nil || !voicecraftali.Retryable(err) {
        return audio, err
    }
    time.Sleep(backoff(attempt))
}
Example

ExampleRetryable 展示按错误码决定是否重试。 退避策略由调用方决定,本库只提供判定。

package main

import (
	"context"
	"log"
	"os"
	"time"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	params := &voicecraftali.SynthesisParams{
		Model: voicecraftali.ModelCosyVoiceV3Flash,
		Voice: "longanyang",
	}

	var audio []byte
	for attempt := range 3 {
		audio, err = client.Synthesize(context.Background(), params, "床前明月光")
		if err == nil {
			break
		}
		if code := voicecraftali.ErrorCode(err); code != "" {
			log.Printf("合成失败 %s: %s", code, voicecraftali.CodeDescription(code))
		}
		if !voicecraftali.Retryable(err) {
			log.Fatal(err) // 参数错误、鉴权失败——重试也没用
		}
		time.Sleep(time.Duration(1<<attempt) * time.Second)
	}
	_ = audio
}

func SupportsSSML

func SupportsSSML(model string) bool

SupportsSSML 判断模型是否支持 SSML 标记语言与 LaTeX 公式朗读。

除模型外还有音色维度的限制:仅复刻音色以及官方音色列表中标注为支持 SSML 的系统音色可用。音色维度无法在本地判定,用不支持的音色时服务端会返回错误。

func SupportsVoiceClone added in v1.1.0

func SupportsVoiceClone(model string) bool

SupportsVoiceClone 判断 model 是否在声音复刻的 target_model 清单内。

回答的仅仅是「该模型是否在官方 target_model 列表内」,不含地域可用性与 账号开通状态,也不含 MiniMax——对 MiniMax 模型名它返回 false, 因为 MiniMax 的复刻不通过 target_model,那条路径见 MiniMaxModels。

返回 false 不代表服务端一定拒绝:清单是快照,官方新增的模型本库尚未收录时 同样返回 false。本库自身不用它拒绝任何请求,调用方要不要据此前置拦截,自行决定。

func SupportsVoiceDesign added in v1.1.0

func SupportsVoiceDesign(model string) bool

SupportsVoiceDesign 判断 model 是否在声音设计的 target_model 清单内。

回答的仅仅是「该模型是否在官方 target_model 列表内」,不含地域可用性与 账号开通状态。对 cosyvoice-v2、cosyvoice-v1 与全部 MiniMax 模型返回 false—— 它们不支持声音设计。

返回 false 不代表服务端一定拒绝:清单是快照,官方新增的模型本库尚未收录时 同样返回 false。本库自身不用它拒绝任何请求,调用方要不要据此前置拦截,自行决定。

func VoiceCloneTargetModels added in v1.1.0

func VoiceCloneTargetModels() []string

VoiceCloneTargetModels 返回全部可作声音复刻 target_model 的模型名。

这些取值可填入 CreateVoiceParams.TargetModel(Qwen-Audio-TTS / CosyVoice) 与 CreateQwenVoiceParams.TargetModel(Qwen-TTS,只接受其中的 qwen3-tts-vc-* 三个)。

清单**不含 MiniMax**:MiniMax 支持声音复刻,但走 CloneMiniMaxVoice, 参数是自己的 Model 字段而不是 target_model,其取值见 MiniMaxModels。

返回的是副本,调用方修改它不会影响本库。

清单是截至 2026-09-03 官方在册模型的快照,仅供参考,本库不用它校验参数: 未收录的模型可以直接传入,是否接受由服务端决定。地域可用性见各模型常量的注释。

func VoiceDesignTargetModels added in v1.1.0

func VoiceDesignTargetModels() []string

VoiceDesignTargetModels 返回全部可作声音设计 target_model 的模型名。

这些取值可填入 DesignVoiceParams.TargetModel(Qwen-Audio-TTS / CosyVoice) 与 DesignQwenVoiceParams.TargetModel(Qwen-TTS,只接受其中的 qwen3-tts-vd-* 三个)。

比声音复刻的清单短:cosyvoice-v2、cosyvoice-v1 与 Qwen-Audio-Realtime 只能复刻, MiniMax 则完全不支持声音设计。

返回的是副本,调用方修改它不会影响本库。

清单是截至 2026-09-03 官方在册模型的快照,仅供参考,本库不用它校验参数: 未收录的模型可以直接传入,是否接受由服务端决定。地域可用性见各模型常量的注释。

func VoiceStatusDescription

func VoiceStatusDescription(s VoiceStatus) string

VoiceStatusDescription 返回音色状态的中文说明。 未收录的状态返回空字符串。

Types

type AudioFormat

type AudioFormat string

AudioFormat 是输出音频的编码格式。

const (
	// FormatPCM 裸 PCM。
	FormatPCM AudioFormat = "pcm"
	// FormatWAV WAV 容器。
	FormatWAV AudioFormat = "wav"
	// FormatMP3 MP3。
	FormatMP3 AudioFormat = "mp3"
	// FormatOpus Opus。cosyvoice-v1 与 Sambert 不支持该格式。
	FormatOpus AudioFormat = "opus"
	// FormatFLAC FLAC。仅 MiniMax 支持该格式。
	FormatFLAC AudioFormat = "flac"
)

type AudioRef

type AudioRef struct {
	// Data 是音频字节。流式模式与 MiniMax 非流式模式下有值。
	Data []byte
	// URL 是音频下载地址,有效期 24 小时。
	// Qwen-Audio-TTS / CosyVoice 与 Qwen-TTS 的非流式模式下有值。
	URL string
	// ID 是服务端为该音频分配的标识。
	ID string
	// ExpiresAt 是 URL 的过期时刻,URL 为空时为零值。
	ExpiresAt time.Time
}

AudioRef 是一段可下载的音频。

Qwen-Audio-TTS / CosyVoice 与 Qwen-TTS 的非流式 HTTP 合成不直接返回音频字节, 而是返回一个有效期 24 小时的下载地址;MiniMax 与所有 API 的流式模式则直接 返回音频字节。两种形态在本类型里共存,用哪一个取决于所调用的接口, 见各方法的文档。

func (*AudioRef) Download

func (a *AudioRef) Download(ctx context.Context, c *Client) ([]byte, error)

Download 通过 URL 取回音频字节。

Data 已有内容时直接返回它,不发起请求。URL 为空且 Data 也为空时返回错误。

下载走的是调用方在 WithHTTPClient 中提供的 HTTP 客户端,因此代理与自定义 TLS 配置同样生效。

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client 是阿里云百炼实时语音合成的客户端。

Client 在 New 返回后不可变,且可以安全地在多个 goroutine 间共享 (safe for concurrent use)。每次创建会话都会新建一条 WebSocket 连接, 会话之间不共享可变状态。

用法:

client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
if err != nil {
    return err
}
session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
    Model: voicecraftali.ModelCosyVoiceV3Flash,
    Voice: "longanyang",
})

func New

func New(opts ...Option) (*Client, error)

New 构造一个 Client。

凭据来源的优先级:WithAPIKey 或 WithCredentialProvider 显式配置 > DASHSCOPE_API_KEY 环境变量 > ErrNoCredential。环境变量未设置或为空字符串 一律视为未配置——空凭据握手必然失败,没有理由让它通过构造期。

其余配置均有默认值,只配鉴权即可使用。

Example

ExampleNew 展示最小构造。

package main

import (
	"fmt"
	"log"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(
		voicecraftali.WithAPIKey("sk-xxx"),
		voicecraftali.WithRegion(voicecraftali.RegionBeijing),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(client != nil)
}
Output:
true
Example (FromEnvironment)

ExampleNew_fromEnvironment 展示从 DASHSCOPE_API_KEY 取凭据。

显式的 WithAPIKey 与 WithCredentialProvider 都优先于环境变量。

package main

import (
	"fmt"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	// 真实使用中这个变量由部署环境提供,这里为了让示例输出确定而就地设置。
	os.Setenv("DASHSCOPE_API_KEY", "sk-from-env")
	defer os.Unsetenv("DASHSCOPE_API_KEY")

	client, err := voicecraftali.New()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(client != nil)
}
Output:
true
Example (MissingCredential)

ExampleNew_missingCredential 展示既没有显式凭据、DASHSCOPE_API_KEY 也没有值时的行为。

package main

import (
	"errors"
	"fmt"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	_, err := voicecraftali.New()
	fmt.Println(errors.Is(err, voicecraftali.ErrNoCredential))
}
Output:
true

func (*Client) CloneMiniMaxVoice

func (c *Client) CloneMiniMaxVoice(
	ctx context.Context,
	params *MiniMaxCloneParams,
) (*MiniMaxCloneResult, error)

CloneMiniMaxVoice 用 MiniMax 复刻一个音色。

与阿里云自研的复刻接口不同:音色 ID 由调用方自定义且全局唯一, 必须提供试听文本,且**试听音频会按所选模型的合成单价额外计费**。

MiniMax 不提供音色列表、详情、更新与删除接口;已复刻音色的查询需前往 控制台的声音管理页面。

result, err := client.CloneMiniMaxVoice(ctx, &voicecraftali.MiniMaxCloneParams{
    Model:    voicecraftali.ModelMiniMaxSpeech28Turbo,
    VoiceID:  "myvoice20260824",
    AudioURL: "https://example.com/sample.wav",
    Text:     "今天天气怎么样?",
})
Example

ExampleClient_CloneMiniMaxVoice 展示 MiniMax 的声音复刻。

音色 ID 由调用方自定义且全局唯一;试听音频会按合成价额外计费。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	result, err := client.CloneMiniMaxVoice(context.Background(), &voicecraftali.MiniMaxCloneParams{
		Model:    voicecraftali.ModelMiniMaxSpeech28Turbo,
		VoiceID:  "myvoice20260824",
		AudioURL: "https://example.com/sample.wav",
		Text:     "今天天气怎么样?",
	})
	if err != nil {
		log.Fatal(err)
	}
	if result.InputSensitive {
		log.Fatalf("输入音频命中风控,类型 %d", result.InputSensitiveType)
	}
	log.Printf("试听音频:%s", result.DemoAudioURL)

	// 复刻出的音色直接用于合成。
	audio, err := client.SynthesizeMiniMax(context.Background(), &voicecraftali.MiniMaxParams{
		Model:        voicecraftali.ModelMiniMaxSpeech28Turbo,
		VoiceSetting: voicecraftali.MiniMaxVoiceSetting{VoiceID: result.VoiceID},
	}, "这是用复刻音色合成的语音。")
	if err != nil {
		log.Fatal(err)
	}
	_ = audio
}

func (*Client) CreateQwenVoice

func (c *Client) CreateQwenVoice(
	ctx context.Context,
	params *CreateQwenVoiceParams,
) (*QwenVoice, error)

CreateQwenVoice 用 Qwen-TTS 复刻一个音色。

样本音频以字节形式传入,本库按协议要求编码成 Data URL。 返回的音色名可直接用作 RealtimeParams.Voice 或 QwenTTSParams.Voice, 但必须搭配 params.TargetModel 指定的那个模型使用。

结果中的 FallbackMode 为 true 时表示复刻是降级完成的,效果可能不理想, 值得在接入流程里显式提示而不是忽略。

voice, err := client.CreateQwenVoice(ctx, &voicecraftali.CreateQwenVoiceParams{
    TargetModel:   voicecraftali.ModelQwen3TTSVCRealtime,
    PreferredName: "myvoice",
    Audio:         &voicecraftali.QwenVoiceAudio{MIME: "audio/wav", Data: sample},
})
Example

ExampleClient_CreateQwenVoice 展示 Qwen-TTS 的声音复刻。

与 CosyVoice 的复刻是两套接口:样本音频以字节内联提交而不是给 URL。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	sample, err := os.ReadFile("sample.wav")
	if err != nil {
		log.Fatal(err)
	}

	voice, err := client.CreateQwenVoice(context.Background(), &voicecraftali.CreateQwenVoiceParams{
		TargetModel:   voicecraftali.ModelQwen3TTSVCRealtime,
		PreferredName: "myvoice",
		Audio:         &voicecraftali.QwenVoiceAudio{MIME: "audio/wav", Data: sample},
		Language:      "zh",
	})
	if err != nil {
		log.Fatal(err)
	}

	// 降级模式意味着样本音频质量不佳,复刻效果可能不理想——值得显式提示。
	if voice.FallbackMode {
		log.Printf("音色以降级模式创建,原因:%s", voice.FallbackReason)
	}
	log.Printf("音色 %s,驱动模型 %s", voice.Voice, voice.TargetModel)
}

func (*Client) CreateVoice

func (c *Client) CreateVoice(ctx context.Context, params *CreateVoiceParams) (string, error)

CreateVoice 用 Qwen-Audio-TTS / CosyVoice 复刻一个音色。

返回的音色 ID 可直接用作 SynthesisParams.Voice 或 HTTPSynthesisParams.Voice, 但**必须搭配 params.TargetModel 指定的那个模型使用**,否则合成会失败。

音色创建后要经过审核:刚返回时状态通常是 DEPLOYING,需轮询 GetVoice 直到状态变为 OK 才能用于合成。

voiceID, err := client.CreateVoice(ctx, &voicecraftali.CreateVoiceParams{
    TargetModel: voicecraftali.ModelCosyVoiceV3Flash,
    Prefix:      "myvoice",
    URL:         "https://example.com/sample.wav",
})
Example

ExampleClient_CreateVoice 展示 Qwen-Audio-TTS / CosyVoice 的声音复刻。

音色创建后要经过审核,状态变为 OK 才能用于合成。

package main

import (
	"context"
	"log"
	"os"
	"time"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// TargetModel 必须与后续合成时使用的模型一致,否则合成会失败。
	voiceID, err := client.CreateVoice(ctx, &voicecraftali.CreateVoiceParams{
		TargetModel: voicecraftali.ModelCosyVoiceV3Flash,
		Prefix:      "myvoice",
		URL:         "https://example.com/sample.wav",
	})
	if err != nil {
		log.Fatal(err)
	}

	// 轮询到审核通过为止。
	for {
		detail, err := client.GetVoice(ctx, voiceID)
		if err != nil {
			log.Fatal(err)
		}
		if detail.Status.Usable() {
			break
		}
		if detail.Status == voicecraftali.VoiceStatusUndeployed {
			log.Fatalf("音色审核未通过:%s", voicecraftali.VoiceStatusDescription(detail.Status))
		}
		time.Sleep(5 * time.Second)
	}

	// 复刻音色的用法与系统音色完全一致。
	audio, err := client.Synthesize(ctx, &voicecraftali.SynthesisParams{
		Model: voicecraftali.ModelCosyVoiceV3Flash, // 与 TargetModel 一致
		Voice: voiceID,
	}, "这是用复刻音色合成的语音。")
	if err != nil {
		log.Fatal(err)
	}
	_ = audio
}

func (*Client) DeleteDesignedQwenVoice

func (c *Client) DeleteDesignedQwenVoice(ctx context.Context, voice string) error

DeleteDesignedQwenVoice 删除一个 Qwen 设计音色。

func (*Client) DeleteQwenVoice

func (c *Client) DeleteQwenVoice(ctx context.Context, voice string) error

DeleteQwenVoice 删除一个 Qwen-TTS 复刻音色。

参数是音色名(创建时返回的 Voice),不是 Qwen-Audio-TTS / CosyVoice 那套 voice_id。

func (*Client) DeleteVoice

func (c *Client) DeleteVoice(ctx context.Context, voiceID string) error

DeleteVoice 删除一个 Qwen-Audio-TTS / CosyVoice 自定义音色。

声音复刻与声音设计产出的音色都用本方法删除。

func (*Client) DesignQwenVoice

func (c *Client) DesignQwenVoice(
	ctx context.Context,
	params *DesignQwenVoiceParams,
) (*DesignedQwenVoice, error)

DesignQwenVoice 用文字描述设计一个 Qwen 音色。

designed, err := client.DesignQwenVoice(ctx, &voicecraftali.DesignQwenVoiceParams{
    TargetModel:   voicecraftali.ModelQwen3TTSVDRealtime,
    VoicePrompt:   "沉稳的中年男性,音色低沉浑厚",
    PreviewText:   "各位听众朋友,大家好",
    PreferredName: "announcer",
})
Example

ExampleClient_DesignQwenVoice 展示 Qwen 的声音设计。

Qwen 侧的设计走独立模型,音色列表与详情也是独立的一套。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	designed, err := client.DesignQwenVoice(ctx, &voicecraftali.DesignQwenVoiceParams{
		TargetModel:   voicecraftali.ModelQwen3TTSVDRealtime,
		VoicePrompt:   "沉稳的中年男性,音色低沉浑厚",
		PreviewText:   "各位听众朋友,大家好",
		PreferredName: "announcer",
		Language:      "zh",
	})
	if err != nil {
		log.Fatal(err)
	}

	// 与声音复刻的音色是两批,用各自的接口管理。
	list, err := client.ListDesignedQwenVoices(ctx, &voicecraftali.ListVoicesParams{PageSize: 20})
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("共 %d 个设计音色,本次新增 %s", list.TotalCount, designed.Voice)
}

func (*Client) DesignVoice

func (c *Client) DesignVoice(
	ctx context.Context,
	params *DesignVoiceParams,
) (*DesignedVoice, error)

DesignVoice 用文字描述设计一个 Qwen-Audio-TTS / CosyVoice 音色。

与 CreateVoice(声音复刻)的区别是输入:设计给文字描述,复刻给样本音频。 两者产出的音色在服务端是同一批,用 ListVoices、GetVoice、DeleteVoice 统一管理。

返回值带一段试听音频,可用来在落库之前先让人听一耳朵。 音色同样要经过审核,需轮询 GetVoice 直到状态为 OK 才能用于合成。

designed, err := client.DesignVoice(ctx, &voicecraftali.DesignVoiceParams{
    TargetModel: voicecraftali.ModelCosyVoiceV35Plus,
    VoicePrompt: "沉稳的中年男性,音色低沉浑厚",
    PreviewText: "各位听众朋友们大家好,欢迎收听本期节目",
    Prefix:      "announcer",
})
Example

ExampleClient_DesignVoice 展示用文字描述设计一个音色。

与声音复刻的区别是输入:设计给描述,复刻给样本音频。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	designed, err := client.DesignVoice(ctx, &voicecraftali.DesignVoiceParams{
		TargetModel: voicecraftali.ModelCosyVoiceV35Plus,
		VoicePrompt: "沉稳的中年男性,音色低沉浑厚",
		PreviewText: "各位听众朋友们大家好,欢迎收听本期节目",
		Prefix:      "announcer", // 设计侧不允许下划线且不超过 10 字符
		Preview: &voicecraftali.PreviewOptions{
			SampleRate:     24000,
			ResponseFormat: voicecraftali.FormatWAV,
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// 先听一耳朵再决定要不要落库。
	if designed.Preview != nil {
		if err := os.WriteFile("preview.wav", designed.Preview.Data, 0o600); err != nil {
			log.Fatal(err)
		}
	}

	// 设计出的音色与复刻音色共用同一套管理接口。
	detail, err := client.GetVoice(ctx, designed.VoiceID)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("音色 %s 设计自「%s」,状态 %s", designed.VoiceID, detail.VoicePrompt, detail.Status)
}

func (*Client) GetDesignedQwenVoice

func (c *Client) GetDesignedQwenVoice(
	ctx context.Context,
	voice string,
) (*DesignedQwenVoiceDetail, error)

GetDesignedQwenVoice 查询 Qwen 设计音色的详情。

声音复刻的 Qwen 侧没有详情接口,声音设计有——这是两者的一处能力差异。

func (*Client) GetVoice

func (c *Client) GetVoice(ctx context.Context, voiceID string) (*VoiceDetail, error)

GetVoice 查询 Qwen-Audio-TTS / CosyVoice 自定义音色的详情。

声音复刻与声音设计的音色都用本方法查询,用 VoiceDetail.Designed 区分来源。

常用于轮询审核状态:创建后状态为 DEPLOYING,变为 OK 才能用于合成。

func (*Client) ListDesignedQwenVoices

func (c *Client) ListDesignedQwenVoices(
	ctx context.Context,
	params *ListVoicesParams,
) (*DesignedQwenVoiceList, error)

ListDesignedQwenVoices 查询 Qwen 设计音色的列表。

与 ListQwenVoices(声音复刻)是两批不同的音色——它们走的是不同的模型。 params 可为 nil。本接口不支持按前缀筛选。

func (*Client) ListQwenVoices

func (c *Client) ListQwenVoices(
	ctx context.Context,
	params *ListVoicesParams,
) (*QwenVoiceList, error)

ListQwenVoices 查询 Qwen-TTS 的复刻音色列表。

与 ListVoices 不同,本接口返回分页信息,且列表项不含审核状态。 params 可为 nil,表示用服务端默认分页。本接口不支持按前缀筛选。

func (*Client) ListVoices

func (c *Client) ListVoices(ctx context.Context, params *ListVoicesParams) ([]VoiceSummary, error)

ListVoices 查询 Qwen-Audio-TTS / CosyVoice 的自定义音色列表。

声音复刻与声音设计产出的音色在服务端是同一批,本方法把它们一并返回; 用 VoiceSummary.Designed 区分来源。

params 可为 nil,表示不筛选、用服务端默认分页。

Example

ExampleClient_ListVoices 展示音色列表与删除。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	voices, err := client.ListVoices(ctx, &voicecraftali.ListVoicesParams{
		Prefix:   "myvoice",
		PageSize: 20,
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, v := range voices {
		log.Printf("%s [%s] %s", v.VoiceID, v.Status, voicecraftali.VoiceStatusDescription(v.Status))
		if v.Status == voicecraftali.VoiceStatusUndeployed {
			if err := client.DeleteVoice(ctx, v.VoiceID); err != nil {
				log.Print(err)
			}
		}
	}
}

func (*Client) NewRealtimeSession

func (c *Client) NewRealtimeSession(
	ctx context.Context,
	params *RealtimeParams,
	opts ...SessionOption,
) (*Session, error)

NewRealtimeSession 创建一个 Qwen-TTS Realtime 会话(WebSocket 事件流)。

params 会被深拷贝,调用方可以安全地复用同一份参数创建多个会话。

交互模式由 params.Mode 决定:server_commit(默认)由服务端自动判定合成时机, 只需持续 SendText;commit 模式需要调用 Commit 才会触发合成。

session, err := client.NewRealtimeSession(ctx, &voicecraftali.RealtimeParams{
    Model: voicecraftali.ModelQwen3TTSFlashRealtime,
    Voice: voicecraftali.VoiceCherry,
    Mode:  voicecraftali.ModeServerCommit,
})
Example

ExampleClient_NewRealtimeSession 展示 Qwen-TTS Realtime 的用法。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	session, err := client.NewRealtimeSession(ctx, &voicecraftali.RealtimeParams{
		Model:        voicecraftali.ModelQwen3TTSFlashRealtime,
		Voice:        voicecraftali.VoiceCherry,
		Mode:         voicecraftali.ModeServerCommit,
		LanguageType: voicecraftali.LanguageChinese,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	go func() {
		_ = session.SendText(ctx, "您好,我是千问。")
		// server_commit 模式下服务端自动判定合成时机;
		// commit 模式则需要显式调用 session.Commit(ctx)。
		_ = session.Finish(ctx)
	}()

	if err := session.Stream(ctx, func(audio []byte) error {
		_ = audio
		return nil
	}); err != nil {
		log.Fatal(err)
	}

	if usage := session.Usage(); usage != nil {
		log.Printf("计费字符数 %d,Token 数 %d", usage.Characters, usage.TotalTokens)
	}
}

func (*Client) NewSambertSession

func (c *Client) NewSambertSession(
	ctx context.Context,
	params *SambertParams,
	opts ...SessionOption,
) (*Session, error)

NewSambertSession 创建一个 Sambert 会话(WebSocket 单向流式)。

params 会被深拷贝,调用方可以安全地复用同一份参数创建多个会话。

Sambert 与 CosyVoice / Qwen-Audio-TTS 是两套不同的 API: 待合成的全文必须放在 params.Text 中随建立会话一次性提交, 会话的 SendText 与 Cancel 都返回 ErrUnsupportedOperation, Finish 是空操作(服务端自行结束任务)。读取音频的方式则完全一致。

Sambert 仅在北京地域可用。

session, err := client.NewSambertSession(ctx, &voicecraftali.SambertParams{
    Model: voicecraftali.ModelSambertZhichu,
    Text:  "床前明月光,疑是地上霜。",
})
if err != nil {
    return err
}
defer session.Close()

err = session.Stream(ctx, func(audio []byte) error {
    _, err := file.Write(audio)
    return err
})
Example

ExampleClient_NewSambertSession 展示 Sambert 的用法。

Sambert 是另一套 API:没有 voice 参数(音色由模型名决定), 全文在创建会话时一次性提交,不支持 SendText。

package main

import (
	"context"
	"errors"
	"io"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	session, err := client.NewSambertSession(ctx, &voicecraftali.SambertParams{
		Model:      voicecraftali.ModelSambertZhichu,
		Text:       "白日依山尽,黄河入海流。",
		Format:     voicecraftali.FormatWAV,
		SampleRate: 16000,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	// 读取方式与其他 API 完全一致。
	for {
		audio, err := session.Read()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		_ = audio
	}
}

func (*Client) NewSynthesisSession

func (c *Client) NewSynthesisSession(
	ctx context.Context,
	params *SynthesisParams,
	opts ...SessionOption,
) (*Session, error)

NewSynthesisSession 创建一个 CosyVoice / Qwen-Audio-TTS 会话(WebSocket 双向流式)。

params 会被深拷贝,调用方可以安全地复用同一份参数创建多个会话。

文本通过 Session.SendText 增量送入,送完调用 Session.Finish。 单次 SendText 上限 20000 字符、单个任务累计上限 200000 字符; 两次发送的间隔不应超过 23 秒,否则服务端会断开连接。

Sambert 模型不能用本方法,请改用 NewSambertSession。

session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
    Model:  voicecraftali.ModelCosyVoiceV3Flash,
    Voice:  "longanyang",
    Format: voicecraftali.FormatMP3,
})
Example

ExampleClient_NewSynthesisSession 展示 CosyVoice / Qwen-Audio-TTS 的流式合成: 一个 goroutine 送文本,主 goroutine 取音频。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
		Model:  voicecraftali.ModelCosyVoiceV3Flash,
		Voice:  "longanyang",
		Format: voicecraftali.FormatMP3,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	go func() {
		// 文本可以分多次送入,边生成边合成。
		for _, line := range []string{"床前明月光,疑是地上霜。", "举头望明月,低头思故乡。"} {
			if err := session.SendText(ctx, line); err != nil {
				log.Print(err)
				return
			}
		}
		// 送完必须调用 Finish,否则服务端不会输出剩余音频。
		if err := session.Finish(ctx); err != nil {
			log.Print(err)
		}
	}()

	file, err := os.Create("output.mp3")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// Stream 会持续把音频交给回调,直到合成结束。
	if err := session.Stream(ctx, func(audio []byte) error {
		_, err := file.Write(audio)
		return err
	}); err != nil {
		log.Fatal(err)
	}
}

func (*Client) StreamHTTP

func (c *Client) StreamHTTP(
	ctx context.Context,
	params *HTTPSynthesisParams,
	text string,
	handler func(audio []byte) error,
	opts ...SessionOption,
) (*SynthesisResult, error)

StreamHTTP 用 Qwen-Audio-TTS / CosyVoice 的非实时 HTTP 接口流式合成。

与 SynthesizeHTTP 的区别:服务端以 SSE 逐段返回 base64 音频分片, handler 每收到一段就被调用一次,适合边合成边写文件或边转发。 handler 返回错误会中止流并把该错误上抛。

开启 params.WordTimestampEnabled 后,进度与字级时间戳通过 opts 中的 WithProgress 回调返回。

返回的 SynthesisResult 来自流的最后一帧,其中 Audio.URL 是完整音频的下载地址, Audio.Data 为空——分片已经通过 handler 交给调用方了。

Example

ExampleClient_StreamHTTP 展示非实时 HTTP 接口的流式模式: 服务端以 SSE 逐段返回音频,边收边写文件。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	file, err := os.Create("output.mp3")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	ctx := context.Background()
	result, err := client.StreamHTTP(ctx, &voicecraftali.HTTPSynthesisParams{
		Model: voicecraftali.ModelCosyVoiceV3Flash,
		Voice: "longanyang",
	}, "床前明月光,疑是地上霜。", func(audio []byte) error {
		_, err := file.Write(audio)
		return err
	})
	if err != nil {
		log.Fatal(err)
	}
	if result.Usage != nil {
		log.Printf("计费字符数 %d", result.Usage.Characters)
	}
}

func (*Client) StreamMiniMax

func (c *Client) StreamMiniMax(
	ctx context.Context,
	params *MiniMaxParams,
	text string,
	handler func(audio []byte) error,
) (*MiniMaxResult, error)

StreamMiniMax 用 MiniMax 的非实时 HTTP 接口流式合成。

handler 每收到一段音频被调用一次(本库已把十六进制解码为字节)。

默认情况下结束帧还会携带一份完整音频,本库**不会**把它再交给 handler, 避免重复;它作为 result.Audio.Data 返回。把 params.ExcludeAggregatedAudio 设为 true 可让服务端不返回这份聚合音频,此时 result.Audio.Data 为空, 完整音频由调用方自行拼接 handler 收到的分片。

func (*Client) StreamQwenTTS

func (c *Client) StreamQwenTTS(
	ctx context.Context,
	params *QwenTTSParams,
	text string,
	handler func(audio []byte) error,
) (*SynthesisResult, error)

StreamQwenTTS 用 Qwen-TTS 的非实时 HTTP 接口流式合成。

服务端以 SSE 逐段返回 base64 音频分片,handler 每收到一段被调用一次。 返回的 SynthesisResult 来自流的最后一帧。

func (*Client) Synthesize

func (c *Client) Synthesize(
	ctx context.Context,
	params *SynthesisParams,
	text string,
) ([]byte, error)

Synthesize 用 CosyVoice / Qwen-Audio-TTS 把一整段文本合成为完整音频。

内部完成建会话、送文本、结束、读干、关闭的全过程,返回时连接已释放。 需要边合成边消费(低首包延迟)时请改用 NewSynthesisSession。

text 长度上限为 20000 字符(按 rune 计),超限返回 ErrTextTooLong 且不建立连接。

合成过程中出错时返回错误且不返回部分音频:一段残缺的音频比没有音频更难排查, 也容易被误当成完整结果写进文件。

audio, err := client.Synthesize(ctx, &voicecraftali.SynthesisParams{
    Model:  voicecraftali.ModelCosyVoiceV3Flash,
    Voice:  "longanyang",
    Format: voicecraftali.FormatMP3,
}, "床前明月光,疑是地上霜。")
Example

ExampleClient_Synthesize 展示短文本一次性合成。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	audio, err := client.Synthesize(context.Background(), &voicecraftali.SynthesisParams{
		Model:  voicecraftali.ModelCosyVoiceV3Flash,
		Voice:  "longanyang",
		Format: voicecraftali.FormatMP3,
	}, "床前明月光,疑是地上霜。")
	if err != nil {
		log.Fatal(err)
	}

	if err := os.WriteFile("output.mp3", audio, 0o600); err != nil {
		log.Fatal(err)
	}
}
Example (LaTeX)

ExampleClient_Synthesize_laTeX 展示 LaTeX 公式朗读。

LaTeX 不需要任何参数开关——把公式用 $...$ 包起来写进文本即可。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	// 用反引号原样字符串,避免为 LaTeX 的反斜杠再转义一层。
	text := `这是一元二次方程的求根公式:$x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$,请仔细计算。`

	audio, err := client.Synthesize(context.Background(), &voicecraftali.SynthesisParams{
		Model: voicecraftali.ModelCosyVoiceV3Flash, // LaTeX 与 SSML 是同一批模型
		Voice: "longanyang",
	}, text)
	if err != nil {
		log.Fatal(err)
	}
	_ = audio
}

func (*Client) SynthesizeHTTP

func (c *Client) SynthesizeHTTP(
	ctx context.Context,
	params *HTTPSynthesisParams,
	text string,
) (*SynthesisResult, error)

SynthesizeHTTP 用 Qwen-Audio-TTS / CosyVoice 的非实时 HTTP 接口合成整段文本。

**返回的是音频下载地址而不是音频字节**:该接口的非流式模式把音频存到 OSS 并返回一个有效期 24 小时的 URL。需要字节时调用 result.Audio.Download:

result, err := client.SynthesizeHTTP(ctx, params, "床前明月光")
if err != nil {
    return err
}
audio, err := result.Audio.Download(ctx, client)

需要边合成边拿到音频字节时用 StreamHTTP。

该接口仅在北京地域可用。

Example

ExampleClient_SynthesizeHTTP 展示 Qwen-Audio-TTS / CosyVoice 的非实时 HTTP 合成。

注意该接口返回的是音频下载地址而不是字节。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := client.SynthesizeHTTP(ctx, &voicecraftali.HTTPSynthesisParams{
		Model:      voicecraftali.ModelQwenAudioTTSFlash,
		Voice:      voicecraftali.VoiceLongAnHuan,
		Format:     voicecraftali.FormatWAV,
		SampleRate: 24000,
	}, "我家的后面有一个很大的花园。")
	if err != nil {
		log.Fatal(err)
	}

	// 非流式模式返回的是有效期 24 小时的下载地址;需要字节时再取一次。
	log.Printf("音频地址 %s(%s 过期)", result.Audio.URL, result.Audio.ExpiresAt)

	audio, err := result.Audio.Download(ctx, client)
	if err != nil {
		log.Fatal(err)
	}
	if err := os.WriteFile("output.wav", audio, 0o600); err != nil {
		log.Fatal(err)
	}
}

func (*Client) SynthesizeMiniMax

func (c *Client) SynthesizeMiniMax(
	ctx context.Context,
	params *MiniMaxParams,
	text string,
) (*MiniMaxResult, error)

SynthesizeMiniMax 用 MiniMax 的非实时 HTTP 接口合成整段文本。

与另两套 HTTP 接口不同,**MiniMax 直接返回音频字节**(线上是十六进制编码, 本库已解码),不返回下载地址,因此 result.Audio.Data 可直接使用。

text 长度上限 10000 字符;超过 3000 字符时官方推荐改用 StreamMiniMax。

Example

ExampleClient_SynthesizeMiniMax 展示 MiniMax 的非实时 HTTP 合成。

与另两套 HTTP 接口不同,MiniMax 直接返回音频字节(线上是十六进制编码, 本库已解码),不需要再下载一次。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	result, err := client.SynthesizeMiniMax(context.Background(), &voicecraftali.MiniMaxParams{
		Model: voicecraftali.ModelMiniMaxSpeech28HD,
		VoiceSetting: voicecraftali.MiniMaxVoiceSetting{
			VoiceID: "male-qn-qingse",
			Speed:   1.0,
			Emotion: voicecraftali.EmotionHappy,
		},
		AudioSetting: voicecraftali.MiniMaxAudioSetting{
			Format:     voicecraftali.FormatMP3,
			SampleRate: 32000,
			Bitrate:    128000,
		},
	}, "今天是不是很开心呀,当然了!")
	if err != nil {
		log.Fatal(err)
	}

	// MiniMax 直接给字节,Audio.Data 可直接使用。
	if err := os.WriteFile("output.mp3", result.Audio.Data, 0o600); err != nil {
		log.Fatal(err)
	}
	log.Printf("时长 %d ms,链路 %s", result.ExtraInfo.AudioLength, result.TraceID)
}

func (*Client) SynthesizeQwenTTS

func (c *Client) SynthesizeQwenTTS(
	ctx context.Context,
	params *QwenTTSParams,
	text string,
) (*SynthesisResult, error)

SynthesizeQwenTTS 用 Qwen-TTS 的非实时 HTTP 接口合成整段文本。

**返回的是音频下载地址而不是音频字节**,需要字节时调用 result.Audio.Download,理由同 SynthesizeHTTP。

文本长度上限:qwen-tts 系列为 512 Token,其余模型为 600 字符。 该上限由服务端判定——Token 数无法在客户端准确计算,因此本库不做本地拦截, 超限会返回服务端的 InvalidInputLength 错误。

Example

ExampleClient_SynthesizeQwenTTS 展示 Qwen-TTS 的非实时 HTTP 合成。

该接口的参数集很小:只有文本、音色、语种与指令。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := client.SynthesizeQwenTTS(ctx, &voicecraftali.QwenTTSParams{
		Model:        voicecraftali.ModelQwen3TTSFlash,
		Voice:        voicecraftali.VoiceCherry,
		LanguageType: voicecraftali.LanguageChinese,
	}, "那我来给大家推荐一款T恤。")
	if err != nil {
		log.Fatal(err)
	}

	audio, err := result.Audio.Download(ctx, client)
	if err != nil {
		log.Fatal(err)
	}
	_ = audio
}

func (*Client) SynthesizeRealtime

func (c *Client) SynthesizeRealtime(
	ctx context.Context,
	params *RealtimeParams,
	text string,
) ([]byte, error)

SynthesizeRealtime 用 Qwen-TTS Realtime 把一整段文本合成为完整音频。

内部会在结束前显式提交一次文本缓冲区,因此 commit 与 server_commit 两种模式下行为一致。

audio, err := client.SynthesizeRealtime(ctx, &voicecraftali.RealtimeParams{
    Model: voicecraftali.ModelQwen3TTSFlashRealtime,
    Voice: voicecraftali.VoiceCherry,
}, "床前明月光,疑是地上霜。")

func (*Client) SynthesizeSambert

func (c *Client) SynthesizeSambert(
	ctx context.Context,
	params *SambertParams,
	text string,
) ([]byte, error)

SynthesizeSambert 用 Sambert 把一整段文本合成为完整音频。

与 Synthesize 的区别只在参数类型:Sambert 没有 voice 参数(音色由模型名决定), 且全文随建立会话一次性提交。params.Text 必须为空——待合成文本由 text 参数提供。

audio, err := client.SynthesizeSambert(ctx, &voicecraftali.SambertParams{
    Model: voicecraftali.ModelSambertZhichu,
}, "床前明月光,疑是地上霜。")

func (*Client) UpdateVoice

func (c *Client) UpdateVoice(ctx context.Context, voiceID, audioURL string) error

UpdateVoice 用新的样本音频更新一个已有的 Qwen-Audio-TTS / CosyVoice 复刻音色。

更新后音色会重新进入审核流程。

type Code

type Code string

Code 是阿里云百炼返回的错误码。

同一类问题在不同 API 下的码名风格不同:两套 WebSocket 合成 API 用 驼峰或点分驼峰(如 InvalidApiKey、Throttling.RateQuota),Qwen-TTS Realtime (Qwen-TTS Realtime)用蛇形(如 invalid_api_key、limit_requests)。 本包把两种风格都收录为常量,调用方无需自行做风格归一。

const (
	// CodeInvalidParameter 请求参数不合法。
	CodeInvalidParameter Code = "InvalidParameter"
	// CodeInvalidParameterDataInspection 内容审核过程中下载媒体资源超时。
	CodeInvalidParameterDataInspection Code = "InvalidParameter.DataInspection"
	// CodeInvalidInputLength 输入长度超出限制。
	CodeInvalidInputLength Code = "InvalidInputLength"
	// CodeBadRequestEmptyInput 请求缺少 input 参数。
	CodeBadRequestEmptyInput Code = "BadRequest.EmptyInput"
	// CodeBadRequestEmptyModel 请求缺少 model 参数。
	CodeBadRequestEmptyModel Code = "BadRequest.EmptyModel"
	// CodeBadRequestEmptyParameters 请求缺少 parameters 参数。
	CodeBadRequestEmptyParameters Code = "BadRequest.EmptyParameters"
	// CodeBadRequestIllegalInput 入参格式不符合 JSON 要求。
	CodeBadRequestIllegalInput Code = "BadRequest.IllegalInput"
	// CodeBadRequestTooLarge 请求体超出大小限制。
	CodeBadRequestTooLarge Code = "BadRequest.TooLarge"
	// CodeBadRequestVoiceNotFound 指定的音色不存在。
	CodeBadRequestVoiceNotFound Code = "BadRequest.VoiceNotFound"
	// CodeBadRequestResourceNotExist 引用的资源不存在。
	CodeBadRequestResourceNotExist Code = "BadRequest.ResourceNotExist"
	// CodeDataInspectionFailed 输入或输出疑似包含敏感内容被拦截。
	CodeDataInspectionFailed Code = "DataInspectionFailed"
	// CodeArrearage 账号欠费导致访问被拒绝。
	CodeArrearage Code = "Arrearage"
	// CodeClientDisconnect 任务结束前客户端主动断开了连接。
	CodeClientDisconnect Code = "ClientDisconnect"
	// CodeUnsupportedOperation 不支持的操作。
	CodeUnsupportedOperation Code = "UnsupportedOperation"

	// CodeInvalidAPIKey API Key 填写错误。
	CodeInvalidAPIKey Code = "InvalidApiKey"
	// CodeNotAuthorized 未授权。
	CodeNotAuthorized Code = "NOT AUTHORIZED"

	// CodeAccessDenied 无权限访问。
	CodeAccessDenied Code = "AccessDenied"
	// CodeAccessDeniedUnpurchased 未开通阿里云百炼服务。
	CodeAccessDeniedUnpurchased Code = "AccessDenied.Unpurchased"
	// CodeModelAccessDenied 无权限调用该模型。
	CodeModelAccessDenied Code = "Model.AccessDenied"
	// CodeWorkspaceAccessDenied 无权限访问该业务空间的模型或应用。
	CodeWorkspaceAccessDenied Code = "Workspace.AccessDenied"
	// CodeEndpointAccessDenied 调用端点不可用,通常因模型已下线。
	CodeEndpointAccessDenied Code = "Endpoint.AccessDenied"
	// CodeAllocationQuotaFreeTierOnly 免费额度已用尽。
	CodeAllocationQuotaFreeTierOnly Code = "AllocationQuota.FreeTierOnly"

	// CodeModelNotFound 模型不存在或无权访问。
	CodeModelNotFound Code = "ModelNotFound"
	// CodeWorkspaceNotFound 业务空间不存在。
	CodeWorkspaceNotFound Code = "WorkSpaceNotFound"
	// CodeNotFound 要查询或操作的资源不存在。
	CodeNotFound Code = "NotFound"
	// CodeConflict 资源冲突,通常是重名。
	CodeConflict Code = "Conflict"

	// CodeThrottling 接口调用触发限流。
	CodeThrottling Code = "Throttling"
	// CodeThrottlingRateQuota 调用频率(RPS/RPM)触发限流。
	CodeThrottlingRateQuota Code = "Throttling.RateQuota"
	// CodeThrottlingBurstRate 调用频率骤增触发系统稳定性保护。
	CodeThrottlingBurstRate Code = "Throttling.BurstRate"
	// CodeThrottlingAllocationQuota Token 消耗(TPS/TPM)触发限流,或资源数量达上限。
	CodeThrottlingAllocationQuota Code = "Throttling.AllocationQuota"
	// CodeThrottlingConcurrency 并发请求数超出平台上限。
	CodeThrottlingConcurrency Code = "Throttling.Concurrency"
	// CodeResourceExhausted 资源耗尽。
	CodeResourceExhausted Code = "ResourceExhausted"
	// CodeLimitRequests 请求数超限。
	CodeLimitRequests Code = "LimitRequests"
	// CodeCommodityNotPurchased 未购买对应商品。
	CodeCommodityNotPurchased Code = "CommodityNotPurchased"
	// CodePrepaidBillOverdue 预付费账单逾期。
	CodePrepaidBillOverdue Code = "PrepaidBillOverdue"
	// CodePostpaidBillOverdue 后付费账单逾期。
	CodePostpaidBillOverdue Code = "PostpaidBillOverdue"

	// CodeInternalError 服务端内部错误。
	CodeInternalError Code = "InternalError"
	// CodeInternalErrorAlgo 推理服务异常。
	CodeInternalErrorAlgo Code = "InternalError.Algo"
	// CodeInternalErrorTimeout 服务端内部超时。
	CodeInternalErrorTimeout Code = "InternalError.Timeout"
	// CodeSystemError 系统错误。
	CodeSystemError Code = "SystemError"
	// CodeModelServiceFailed 模型服务调用失败。
	CodeModelServiceFailed Code = "ModelServiceFailed"
	// CodeRequestTimeOut 请求超时。
	CodeRequestTimeOut Code = "RequestTimeOut"
	// CodeResponseTimeout 响应流超时。
	CodeResponseTimeout Code = "ResponseTimeout"
	// CodeServiceUnavailableError 服务不可用。
	CodeServiceUnavailableError Code = "ServiceUnavailableError"
	// CodeModelUnavailable 模型暂时无法提供服务。
	CodeModelUnavailable Code = "ModelUnavailable"
)

WebSocket 合成 API 风格返回码(CosyVoice / Qwen-Audio-TTS / Sambert,驼峰命名)。

const (
	// CodeInvalidValue 参数取值不合法,例如所选音色与模型不匹配。
	CodeInvalidValue Code = "invalid_value"
	// CodeInvalidRequestError 请求不合法。
	CodeInvalidRequestError Code = "invalid_request_error"
	// CodeDataInspectionFailedSnake 输入或输出疑似包含敏感内容被拦截。
	CodeDataInspectionFailedSnake Code = "data_inspection_failed"
	// CodeInvalidAPIKeySnake API Key 填写错误。
	CodeInvalidAPIKeySnake Code = "invalid_api_key"
	// CodeAccessDeniedSnake 无权限访问。
	CodeAccessDeniedSnake Code = "access_denied"
	// CodeModelNotFoundSnake 模型不存在或无权访问。
	CodeModelNotFoundSnake Code = "model_not_found"
	// CodeModelNotSupported 模型不支持该调用方式。
	CodeModelNotSupported Code = "model_not_supported"
	// CodeLimitRequestsSnake 调用频率触发限流。
	CodeLimitRequestsSnake Code = "limit_requests"
	// CodeLimitBurstRate 调用频率骤增触发系统稳定性保护。
	CodeLimitBurstRate Code = "limit_burst_rate"
	// CodeInsufficientQuota 配额不足。
	CodeInsufficientQuota Code = "insufficient_quota"
	// CodeInternalErrorSnake 服务端内部错误。
	CodeInternalErrorSnake Code = "internal_error"
)

Qwen-TTS Realtime 风格返回码(蛇形命名)。

func ErrorCode

func ErrorCode(err error) Code

ErrorCode 从错误中提取阿里云返回码。

对携带返回码的错误(*TaskFailedError、*EventError、*HTTPError)返回其 Code; 其余错误(含 nil)返回空字符串。

与 CodeDescription、Retryable 配合可在不做类型断言的前提下处理服务端错误:

if code := voicecraftali.ErrorCode(err); code != "" {
    log.Printf("合成失败 %s: %s", code, voicecraftali.CodeDescription(code))
}

type CreateQwenVoiceParams

type CreateQwenVoiceParams struct {
	// TargetModel 是驱动该音色的语音合成模型。必填。
	// 必须与后续合成时使用的模型一致。
	//
	// 本接口只接受 Qwen-TTS 声音复刻的三个模型:ModelQwen3TTSVC、
	// ModelQwen3TTSVCRealtime、ModelQwen3TTSVCRealtimePrev——都是 qwen3-tts-vc-*
	// 前缀。声音设计的 qwen3-tts-vd-* 在这里用不了。
	// 全部可用取值见 VoiceCloneTargetModels。
	TargetModel string
	// PreferredName 是音色名称前缀,仅允许数字、英文字母与下划线,不超过 16 个字符。必填。
	PreferredName string
	// Audio 是样本音频。必填。
	Audio *QwenVoiceAudio
	// Language 是样本音频的语种,零值表示不设置(服务端默认 zh)。
	Language string
}

CreateQwenVoiceParams 是 Qwen-TTS 声音复刻的创建参数。

与 Qwen-Audio-TTS / CosyVoice 的复刻是两套不同的接口: 样本音频以 Data URL 内联提交而不是给 URL,音色名字段叫 preferred_name 而非 prefix, 语种字段是单值 language 而非数组 language_hints。

type CreateVoiceParams

type CreateVoiceParams struct {
	// TargetModel 是驱动该音色的语音合成模型。必填。
	//
	// 它**必须与后续合成时使用的模型一致**,否则合成会失败——
	// 这是复刻音色最常见的踩坑点。
	//
	// 取值参见 VoiceCloneTargetModels:声音复刻与声音设计能用的模型不是同一批,
	// 例如 cosyvoice-v2 与 cosyvoice-v1 只能复刻、不能设计。本字段只校验非空,
	// 要在发请求前拦下错配,调用方自己用 SupportsVoiceClone 判断。
	TargetModel string
	// Prefix 是音色名称前缀,仅允许数字、英文字母与下划线,不超过 10 个字符。必填。
	// 服务端会在其基础上拼出完整的 voice_id(形如 <target_model>-<prefix>-<uuid>)。
	//
	// 上限 10 与 Qwen-TTS 侧 CreateQwenVoiceParams.PreferredName 的 16 不同,
	// 是两套服务端规则,不是笔误。
	Prefix string
	// URL 是用于复刻的音频文件地址,要求公网可访问。必填。
	URL string

	// LanguageHints 辅助模型识别样本音频的语种以更准确地提取音色特征。
	// 服务端只处理第一个元素,默认 ["zh"]。
	// 设置的语种与实际音频不符时服务端会忽略该设置并自动检测。
	LanguageHints []string

	// MaxPromptAudioLength 是音频预处理后用于复刻的参考音频最大时长(秒),
	// 取值范围 [3.0, 30.0],零值表示不设置(服务端默认 10.0)。
	MaxPromptAudioLength float64

	// EnablePreprocess 开启音频预处理(降噪、音频增强、音量规整)。
	// 有背景噪音时建议开启;安静环境建议关闭以最大程度还原音色。
	EnablePreprocess bool

	// EnableVolumeNormalization 对样本音频做音量归一化。
	//
	// 用指针而非 bool:该字段在协议上是**字符串** "true"/"false",
	// 且开启与否会改变合成音量,因此需要区分「显式关闭」与「不设置」。
	EnableVolumeNormalization *bool
}

CreateVoiceParams 是 Qwen-Audio-TTS / CosyVoice 声音复刻的创建参数。

type Credential

type Credential struct {
	// Value 是 API Key 本身,会以 "bearer <Value>" 的形式写入 Authorization 头。
	Value string
	// ExpiresAt 是该凭据的绝对过期时间。
	// 零值表示永不过期(适合本身就是长期密钥、但调用方希望从外部注入的场景)。
	ExpiresAt time.Time
}

Credential 是一份用于握手鉴权的凭据。

type CredentialProvider

type CredentialProvider func(ctx context.Context) (Credential, error)

CredentialProvider 由调用方实现,用于提供动态凭据。

适用于短期密钥、STS 临时凭证、密钥托管系统轮转等场景。本库负责缓存与 并发去重:在凭据过期前不会重复调用 provider,且并发请求只会触发一次调用。

provider 应当尊重传入的 ctx;返回空 Value 会被判为 ErrInvalidCredential 且不入缓存。

type DesignQwenVoiceParams

type DesignQwenVoiceParams struct {
	// TargetModel 是驱动该音色的语音合成模型,如 ModelQwen3TTSVDRealtime。必填。
	//
	// 本接口只接受 Qwen-TTS 声音设计的三个模型:ModelQwen3TTSVD、
	// ModelQwen3TTSVDRealtime、ModelQwen3TTSVDRealtimePrev——都是 qwen3-tts-vd-*
	// 前缀。声音复刻的 qwen3-tts-vc-* 在这里用不了,反之亦然。
	// 全部可用取值见 VoiceDesignTargetModels。
	TargetModel string
	// VoicePrompt 是声音描述文本。必填。不超过 2048 字符。
	VoicePrompt string
	// PreviewText 是预览音频对应的文本。必填。不超过 1024 字符。
	PreviewText string
	// PreferredName 是音色名称前缀。必填。
	// 仅允许数字、英文字母与下划线,不超过 16 个字符。
	PreferredName string

	// Language 指定生成音色的语言倾向,零值表示不设置(服务端默认 zh)。
	// 该语种需与 PreviewText 的语种一致。
	Language string

	// Preview 配置预览音频的采样率与格式。
	Preview *PreviewOptions
}

DesignQwenVoiceParams 是 Qwen 声音设计的参数。

与 Qwen 声音复刻走的是**不同的模型**(qwen-voice-design 对 qwen-voice-enrollment),因此音色列表、详情、删除也都是独立的一套。

type DesignVoiceParams

type DesignVoiceParams struct {
	// TargetModel 是驱动该音色的语音合成模型。必填。
	// 必须与后续合成时使用的模型一致,否则合成会失败。
	//
	// 取值参见 VoiceDesignTargetModels,它比声音复刻的清单短:
	// cosyvoice-v2、cosyvoice-v1 与 Qwen-Audio-Realtime 只能复刻、不能设计。
	// 本字段只校验非空,要在发请求前拦下错配,调用方自己用
	// SupportsVoiceDesign 判断。
	TargetModel string
	// VoicePrompt 是声音描述文本,如"沉稳的中年男性,音色低沉浑厚"。必填。
	// 仅支持中文与英文,不超过 500 字符。
	VoicePrompt string
	// PreviewText 是预览音频对应的文本。必填。
	// 长度需在 [15, 200] 字符之间,支持中文与英文。
	PreviewText string
	// Prefix 是音色名称前缀。必填。
	//
	// 约束比声音复刻更严:**仅允许数字与英文字母(不含下划线)**,
	// 且不超过 10 个字符。生成的音色名形如 {target_model}-vd-{prefix}-{唯一标识}。
	Prefix string

	// LanguageHints 指定生成音色的语言倾向,仅支持 zh 与 en,默认 ["zh"]。
	// 服务端只处理第一个元素,且该语种需与 PreviewText 的语种一致。
	LanguageHints []string

	// Preview 配置预览音频的采样率与格式。
	Preview *PreviewOptions
}

DesignVoiceParams 是 Qwen-Audio-TTS / CosyVoice 声音设计的参数。

与声音复刻(CreateVoiceParams)走的是同一个接口,靠 input 字段区分: 设计给的是文字描述而不是样本音频。若干约束比复刻更严,见各字段说明。

type DesignedQwenVoice

type DesignedQwenVoice struct {
	// Voice 是设计出的音色名,可直接用作 RealtimeParams.Voice 或 QwenTTSParams.Voice。
	Voice string
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string
	// Preview 是试听音频,服务端未返回时为 nil。
	Preview *PreviewAudio
}

DesignedQwenVoice 是一次 Qwen 声音设计的结果。

type DesignedQwenVoiceDetail

type DesignedQwenVoiceDetail struct {
	// Voice 是音色名。
	Voice string
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string
	// Language 是生成音色时的语言倾向。
	Language string
	// GmtCreate 是创建时间。
	GmtCreate string
	// GmtModified 是修改时间。
	GmtModified string
}

DesignedQwenVoiceDetail 是 Qwen 设计音色的详情。

type DesignedQwenVoiceList

type DesignedQwenVoiceList struct {
	// Voices 是本页音色。
	Voices []DesignedQwenVoiceSummary
	// PageIndex 是当前页码索引。
	PageIndex int
	// PageSize 是每页数据条数。
	PageSize int
	// TotalCount 是音色总数。
	TotalCount int
}

DesignedQwenVoiceList 是 Qwen 设计音色列表的分页结果。

type DesignedQwenVoiceSummary

type DesignedQwenVoiceSummary struct {
	// Voice 是音色名。
	Voice string `json:"voice"`
	// GmtCreate 是创建时间。
	GmtCreate string `json:"gmt_create"`
	// GmtModified 是修改时间。
	GmtModified string `json:"gmt_modified"`
	// Language 是生成音色时的语言倾向。
	Language string `json:"language"`
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string `json:"target_model"`
	// VoicePrompt 是声音描述文本。
	VoicePrompt string `json:"voice_prompt"`
	// PreviewText 是预览文本。
	PreviewText string `json:"preview_text"`
}

DesignedQwenVoiceSummary 是 Qwen 设计音色列表中的一项。

type DesignedVoice

type DesignedVoice struct {
	// VoiceID 是设计出的音色 ID,可直接用作合成参数中的 Voice。
	VoiceID string
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string
	// Preview 是试听音频,服务端未返回时为 nil。
	Preview *PreviewAudio
}

DesignedVoice 是一次 Qwen-Audio-TTS / CosyVoice 声音设计的结果。

type EventError

type EventError struct {
	// EventID 是服务端事件 ID。
	EventID string
	// Code 取服务端 error.code。可传给 CodeDescription 获取中文说明。
	Code Code
	// Message 取服务端 error.message,是服务端原始文案。
	Message string
}

EventError 表示 Qwen-TTS Realtime 的服务端返回了 error 事件。

func (*EventError) Error

func (e *EventError) Error() string

type HTTPError

type HTTPError struct {
	// StatusCode 是 HTTP 响应状态码。
	StatusCode int
	// Code 取响应体的 code 字段。可传给 CodeDescription 获取中文说明。
	Code Code
	// Message 取响应体的 message 字段,是服务端原始文案。
	Message string
	// RequestID 是本次调用的唯一标识符,提交工单排查时需要提供。
	RequestID string
	// RetryAfter 是服务端 Retry-After 响应头给出的建议等待时长,
	// 服务端未给该头、或其取值无法解析时为 0。
	//
	// 本库不依据该字段自行重试——重试与退避的策略由调用方决定,
	// 这里只是把服务端已经给出的依据交出来,省得调用方拍一个数字:
	//
	//	if voicecraftali.Retryable(err) {
	//	    wait := time.Second
	//	    if httpErr, ok := errors.AsType[*voicecraftali.HTTPError](err); ok && httpErr.RetryAfter > 0 {
	//	        wait = httpErr.RetryAfter
	//	    }
	//	    time.Sleep(wait)
	//	}
	RetryAfter time.Duration
}

HTTPError 表示非实时 HTTP 合成接口返回了失败响应。

阿里云在 HTTP 侧用响应体里的 code / message 表达业务错误, 与 WebSocket 侧的 task-failed / error 事件是不同的载体,因此单列一个类型。

func (*HTTPError) Error

func (e *HTTPError) Error() string

type HTTPSynthesisParams

type HTTPSynthesisParams struct {
	// Model 是模型名。必填。取值见 httpSynthesisModels,不含 cosyvoice-v1。
	Model string
	// Voice 是音色。必填。
	Voice string

	// Format 是输出音频格式,零值表示不设置。
	//
	// 服务端在不设置时给什么格式,本路径**未经实测**;WebSocket 合成路径
	// (SynthesisParams.Format)实测返回的是裸 PCM 而非 mp3。要拿到确定的格式,
	// 显式指定即可。
	Format AudioFormat
	// SampleRate 是输出采样率(Hz),取值 8000、16000、22050、24000、44100 或 48000,
	// 零值表示不设置(服务端默认 22050)。
	SampleRate int

	// Volume 是音量,取值范围 [0, 100],服务端默认 50。
	// 用指针的原因同 SynthesisParams.Volume。
	Volume *int
	// Rate 是语速,取值范围 [0.5, 2.0],零值表示不设置。
	Rate float64
	// Pitch 是语调,取值范围 [0.5, 2.0],零值表示不设置。
	Pitch float64
	// BitRate 是码率(kbps),取值范围 [6, 510],**仅 opus 格式有效**,
	// 设了别的格式(含 Format 留零值)会被拒绝。
	// 零值表示不设置(服务端默认 32)。
	BitRate int
	// Seed 是随机数种子,取值范围 [0, 65535]。
	Seed int

	// EnableSSML 启用 SSML 解析,此时 text 需为 SSML 标记文本。
	//
	// 仅 CosyVoice 系列的部分模型支持(见 SupportsSSML)。
	// 拼接 SSML 时外部文本需先过 EscapeSSMLText 转义。
	EnableSSML bool
	// WordTimestampEnabled 启用字级别时间戳,仅流式模式下有效,
	// 时间戳随流式回调的 Progress 返回。
	WordTimestampEnabled bool
	// EnableMarkdownFilter 在合成前过滤文本中的 Markdown 标记符号。
	EnableMarkdownFilter bool

	// LanguageHints 指定目标语言以提升合成效果,服务端只处理第一个元素。
	LanguageHints []string
	// Instruction 是自然语言指令,用于控制方言、情感或角色等合成效果。
	Instruction string

	// EnableAIGCTag 在生成的音频中嵌入 AIGC 隐性标识。
	EnableAIGCTag bool
	// AIGCPropagator 设置 AIGC 隐性标识的 ContentPropagator 字段。
	AIGCPropagator string
	// AIGCPropagateID 设置 AIGC 隐性标识的 PropagateID 字段。
	AIGCPropagateID string

	// HotFix 是文本热修复配置。cosyvoice-v2 不支持。
	HotFix *HotFix
}

HTTPSynthesisParams 是 Qwen-Audio-TTS / CosyVoice 非实时 HTTP 合成的参数。

与 WebSocket 侧的 SynthesisParams 单列成两个类型,是因为线格式不同: 本类型的所有字段都在 input 内,而不是独立的 parameters 对象。

该接口仅在北京地域可用。

type HandshakeError

type HandshakeError struct {
	// StatusCode 是握手响应的 HTTP 状态码;握手未走到响应阶段时为 0。
	StatusCode int
	// URL 是本次握手的目标地址,不含任何凭据。
	URL string
	// contains filtered or unexported fields
}

HandshakeError 表示 WebSocket 握手失败。

鉴权在握手阶段完成,因此 API Key 无效或缺失表现为 StatusCode 401 或 403。

func (*HandshakeError) Error

func (e *HandshakeError) Error() string

func (*HandshakeError) Unwrap

func (e *HandshakeError) Unwrap() error

Unwrap 返回底层 dial 错误,使 errors.Is / errors.As 能穿透到根因。

type HotFix

type HotFix struct {
	// Pronunciation 是自定义发音,每个元素形如 {"天气": "tian1 qi4"}。
	Pronunciation []map[string]string `json:"pronunciation,omitempty"`
	// Replace 是文本替换,每个元素形如 {"今天": "金天"},替换后的文本作为实际合成内容。
	Replace []map[string]string `json:"replace,omitempty"`
}

HotFix 是文本热修复配置,用于纠正指定词语的发音或在合成前替换文本。

cosyvoice-v2 与 cosyvoice-v1 不支持该功能,Sambert 没有该参数。

type ListVoicesParams

type ListVoicesParams struct {
	// Prefix 按前缀筛选音色,留空表示不筛选。
	Prefix string
	// PageIndex 是页码索引,从 0 开始。
	PageIndex int
	// PageSize 是每页数据条数,零值表示用服务端默认。
	PageSize int
}

ListVoicesParams 是查询音色列表的参数。

type MiniMaxAudioSetting

type MiniMaxAudioSetting struct {
	// SampleRate 是采样率(Hz),零值表示不设置(服务端默认 32000)。
	SampleRate int
	// Bitrate 是码率(bps,注意单位不是 kbps),零值表示不设置。
	Bitrate int
	// Format 是音频格式,取值 mp3、pcm、flac 或 wav,零值表示不设置。
	Format AudioFormat
	// Channel 是声道数,取值 1 或 2,零值表示不设置(服务端默认 1)。
	Channel int
	// ForceCBR 以恒定码率编码。仅流式输出且格式为 mp3 时生效。
	ForceCBR bool
}

MiniMaxAudioSetting 是 MiniMax 的音频编码设置。

type MiniMaxCloneParams

type MiniMaxCloneParams struct {
	// Model 是合成试听音频所用的模型,见 ModelMiniMaxSpeech28HD 等常量或
	// MiniMaxModels。必填。
	//
	// 注意这里叫 Model 而不是 TargetModel:MiniMax 的复刻走独立接口,
	// 不使用 target_model,因此 MiniMax 模型名**不出现在**
	// VoiceCloneTargetModels 里,SupportsVoiceClone 对它们也返回 false。
	// 与另外两套复刻不同,本字段做白名单校验,取值必须是 MiniMaxModels 之一。
	Model string
	// VoiceID 是自定义的音色 ID。必填,且**全局唯一**。
	//
	// 约束:长度 [8, 256],首字符为英文字母,允许数字、字母、连字符与下划线,
	// 末位不可为连字符或下划线。与已有 ID 重复会被服务端拒绝,
	// 建议带上时间戳等个性化信息。
	VoiceID string
	// AudioURL 是待复刻的音频地址。必填。
	// 要求 mp3/m4a/wav、时长 10 秒到 5 分钟、不超过 20 MB。
	AudioURL string
	// Text 是试听内容,不超过 1000 字符。必填。
	//
	// 试听音频按所选模型的合成单价**额外计费**。
	Text string

	// ClonePrompt 是可选的示例音频,用于提升相似度与稳定性。
	ClonePrompt *MiniMaxClonePrompt

	// LanguageBoost 增强对指定小语种或方言的识别能力,可设为 "auto"。
	LanguageBoost string
	// NeedNoiseReduction 开启降噪。
	NeedNoiseReduction bool
	// NeedVolumeNormalization 开启音量归一化。
	NeedVolumeNormalization bool
	// AIGCWatermark 在试听音频末尾添加音频节奏标识。
	AIGCWatermark bool
}

MiniMaxCloneParams 是 MiniMax 声音复刻的参数。

与阿里云自研的两套复刻接口差异很大:voice_id 由调用方自定义且全局唯一, 必须提供一段试听文本(会按合成价另行计费),且不提供列表、详情、更新操作。

type MiniMaxClonePrompt

type MiniMaxClonePrompt struct {
	// PromptAudio 是示例音频地址。要求 mp3/m4a/wav、时长小于 8 秒、不超过 20 MB。
	PromptAudio string `json:"prompt_audio,omitempty"`
	// PromptText 是示例音频对应的文本,需与音频内容一致且句末带标点。
	PromptText string `json:"prompt_text,omitempty"`
}

MiniMaxClonePrompt 是 MiniMax 声音复刻的示例音频, 提供它有助于提升音色相似度与稳定性。

type MiniMaxCloneResult

type MiniMaxCloneResult struct {
	// RequestID 是本次调用的唯一标识符。
	RequestID string
	// VoiceID 是复刻出的音色 ID,即请求中自定义的那个值,
	// 可直接用作 MiniMaxVoiceSetting.VoiceID。
	VoiceID string
	// DemoAudioURL 是试听音频的地址。
	DemoAudioURL string
	// InputSensitive 为 true 表示输入音频命中了风控。
	InputSensitive bool
	// InputSensitiveType 是命中风控的类型,见 SensitiveNone 等常量。
	InputSensitiveType int
	// Usage 是本次调用的计费信息——试听音频按合成价另行计费。
	Usage *Usage
}

MiniMaxCloneResult 是一次 MiniMax 声音复刻的结果。

type MiniMaxExtraInfo

type MiniMaxExtraInfo struct {
	AudioChannel    int    `json:"audio_channel"`
	AudioFormat     string `json:"audio_format"`
	AudioLength     int    `json:"audio_length"`
	AudioSampleRate int    `json:"audio_sample_rate"`
	AudioSize       int    `json:"audio_size"`
	Bitrate         int    `json:"bitrate"`
	UsageCharacters int    `json:"usage_characters"`
	WordCount       int    `json:"word_count"`
}

MiniMaxExtraInfo 是 MiniMax 返回的音频元信息。

type MiniMaxParams

type MiniMaxParams struct {
	// Model 是模型名,见 ModelMiniMaxSpeech28HD 等常量或 MiniMaxModels。必填。
	// 本字段做白名单校验,取值必须是 MiniMaxModels 之一。
	Model string
	// VoiceSetting 是音色与韵律设置。必填。
	VoiceSetting MiniMaxVoiceSetting
	// AudioSetting 是音频编码设置,各字段留零值即用服务端默认。
	AudioSetting MiniMaxAudioSetting

	// TimbreWeights 是混合音色配置,最多 4 种。
	// 使用混合音色时 VoiceSetting.VoiceID 需留空。
	TimbreWeights []MiniMaxTimbreWeight

	// PronunciationTone 是注音与发音替换规则,用 / 分隔,
	// 如 "燕少飞/(yan4)(shao3)(fei1)"、"omg/oh my god"。
	PronunciationTone []string

	// LanguageBoost 增强对指定小语种或方言的识别能力,可设为 "auto" 让模型自行判断。
	LanguageBoost string
	// SubtitleEnable 启用字幕输出。
	SubtitleEnable bool
	// ExcludeAggregatedAudio 控制流式结束帧是否携带完整音频。
	// 设为 true 时结束帧的音频为空,需由调用方自行拼接分片,可减少尾包体积。
	// 仅流式模式生效。
	ExcludeAggregatedAudio bool
}

MiniMaxParams 是 MiniMax 非实时 HTTP 合成的参数。

MiniMax 是第三方模型,参数结构与阿里云自研模型完全不同:韵律与音频设置各自 成组,音量取值域是 (0, 10] 而不是 [0, 100],音高是 [-12, 12] 的整数, 且**返回的音频是十六进制编码**而不是 base64。

type MiniMaxResult

type MiniMaxResult struct {
	SynthesisResult
	// ExtraInfo 是音频元信息(时长、体积、实际采样率等),服务端未返回时为 nil。
	ExtraInfo *MiniMaxExtraInfo
	// TraceID 是 MiniMax 侧的链路标识,与 RequestID 是两套体系。
	TraceID string
}

MiniMaxResult 是一次 MiniMax 合成的结果。

type MiniMaxTimbreWeight

type MiniMaxTimbreWeight struct {
	// VoiceID 是音色 ID。必填。
	VoiceID string `json:"voice_id"`
	// Weight 是权重,取值范围 [1, 100]。必填。
	Weight int `json:"weight"`
}

MiniMaxTimbreWeight 是混合音色中的一个成分。

type MiniMaxVoiceSetting

type MiniMaxVoiceSetting struct {
	// VoiceID 是音色 ID。使用混合音色时留空并填 TimbreWeights。
	VoiceID string
	// Speed 是语速,取值范围 [0.5, 2.0],零值表示不设置(服务端默认 1.0)。
	Speed float64
	// Vol 是音量,取值范围 (0.0, 10.0],零值表示不设置(服务端默认 1.0)。
	Vol float64
	// Pitch 是音高,取值范围 [-12, 12],服务端默认 0。
	// 用指针:0 是合法取值也是默认值,但显式设 0 与不设在语义上一致,
	// 这里仍用指针以便与其他可选项保持一致的表达方式。
	Pitch *int
	// Emotion 是情感,零值表示不设置(模型自动匹配)。
	Emotion string
	// TextNormalization 启用中英文文本规范化,提升数字阅读场景表现。
	TextNormalization bool
	// LatexRead 启用 LaTeX 公式朗读。仅支持中文,开启后 LanguageBoost 会被设为 Chinese。
	LatexRead bool
}

MiniMaxVoiceSetting 是 MiniMax 的音色与韵律设置。

type Option

type Option func(*config) error

Option 配置 Client。非法取值一律返回错误,不静默回退默认值——静默回退会让 配置写错的调用方在生产环境里得到一个「看起来能跑但行为不对」的客户端。

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey 配置静态 API Key 鉴权。

注意:北京地域与新加坡地域的 API Key 不通用,需与 WithRegion 匹配。

func WithAudioBuffer

func WithAudioBuffer(frames int) Option

WithAudioBuffer 配置音频帧缓冲容量(帧数),默认 64。

缓冲写满时后台接收会阻塞等待,把反压传导给服务端而不是丢弃音频帧。 调大可以容忍更抖动的消费速率,代价是更高的内存占用上限。

func WithBaseURL

func WithBaseURL(raw string) Option

WithBaseURL 用完整的 WebSocket 基址覆盖默认端点,覆盖后地域与专属域名配置不再生效。

接受 ws、wss、http、https 四种 scheme,http 与 https 会被分别改写为 ws 与 wss。 可带路径前缀(如网关场景 wss://gw.example.com/aliyun),协议路径会拼在其后。

func WithCredentialProvider

func WithCredentialProvider(p CredentialProvider) Option

WithCredentialProvider 配置动态凭据鉴权,用于短期密钥、STS 临时凭证或 密钥托管系统轮转等场景。

本库负责缓存与并发去重:凭据在过期前被复用,且并发请求只会触发一次 provider 调用。详见 CredentialProvider。

Example

ExampleWithCredentialProvider 展示动态凭据:适用于短期密钥、STS 临时凭证 或密钥托管系统轮转。本库负责缓存与并发去重。

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(
		voicecraftali.WithCredentialProvider(func(ctx context.Context) (voicecraftali.Credential, error) {
			// 这里换成真实的密钥获取逻辑。
			key, expiresAt, err := fetchShortLivedKey(ctx)
			if err != nil {
				return voicecraftali.Credential{}, err
			}
			return voicecraftali.Credential{Value: key, ExpiresAt: expiresAt}, nil
		}),
		// 提前 1 分钟刷新,避开「刚好在传输途中过期」。
		voicecraftali.WithCredentialRefresh(time.Minute, 10*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(client != nil)
}

func fetchShortLivedKey(context.Context) (string, time.Time, error) {
	return "sk-temp", time.Now().Add(time.Hour), nil
}
Output:
true

func WithCredentialRefresh

func WithCredentialRefresh(skew, timeout time.Duration) Option

WithCredentialRefresh 配置动态凭据的刷新行为。

skew 是过期提前量:凭据在距过期不足 skew 时被视为已过期并触发刷新, 用于规避「刚好在传输途中过期」导致的偶发 401。 timeout 是单次 provider 调用的时长上限。

默认 skew 与 timeout 均为 30 秒。

func WithDataInspection

func WithDataInspection() Option

WithDataInspection 启用 X-DashScope-DataInspection: enable 请求头, 让服务端对输入输出做内容安全检查。

启用后若文本命中拦截规则,服务端返回 CodeDataInspectionFailed。

func WithDedicatedEndpoint

func WithDedicatedEndpoint() Option

WithDedicatedEndpoint 启用业务空间专属域名({workspaceID}.{region}.maas.aliyuncs.com)。

阿里云建议从共享域名迁移到专属域名以获得更稳定的推理性能。 启用时必须同时配置 WithWorkspaceID,否则 New 返回校验错误。

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient 提供一个 http.Client,本库从其 Transport 继承代理、 自定义 TLS 配置与自定义拨号函数。Transport 按引用共享,因此连接池、 keep-alive 与 HTTP/2 与调用方的其他请求一并复用。

该 Client 的 Timeout 字段在 WebSocket 与非实时 HTTP 两条路径上都不生效: 它覆盖「连接建立到响应体读完」的全过程,会把正常的 SSE 音频流拦腰截断。 单次调用的超时请用传入各方法的 ctx 控制;WebSocket 的握手与读空闲另由 WithHandshakeTimeout 与 WithReadIdleTimeout 控制。

传入的 http.Client 实例不会被本库修改——Timeout 的清零只作用于内部副本, 因此可以安全地把进程内共享的 http.Client 传进来。

func WithHandshakeTimeout

func WithHandshakeTimeout(d time.Duration) Option

WithHandshakeTimeout 配置 WebSocket 握手超时,默认 10 秒。

func WithReadIdleTimeout

func WithReadIdleTimeout(d time.Duration) Option

WithReadIdleTimeout 配置单次读取的空闲超时,默认 60 秒。

每次成功读取后续期。作用是让半开连接(服务端进程消失但 TCP 未收到 FIN) 表现为读超时错误,而不是让读取 goroutine 永久阻塞。

func WithRegion

func WithRegion(r Region) Option

WithRegion 配置服务地域,默认 RegionBeijing。

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent 配置 User-Agent 请求头,便于服务端追踪调用来源。 默认为 "gtkit-voicecraft-ali/<version>"。

func WithWorkspaceID

func WithWorkspaceID(id string) Option

WithWorkspaceID 配置阿里云百炼业务空间 ID。

该 ID 会写入 X-DashScope-WorkSpace 请求头;若同时调用 WithDedicatedEndpoint, 还会用于拼出业务空间专属域名。

func WithWriteTimeout

func WithWriteTimeout(d time.Duration) Option

WithWriteTimeout 配置单次写入的超时,默认 10 秒。

type Phoneme

type Phoneme struct {
	// Text 是音素文本。
	Text string `json:"text"`
	// Tone 是音调。文档中该字段为整数(如 2)。
	Tone int `json:"tone"`
	// BeginTime 是音素起始时间(毫秒)。
	BeginTime int `json:"begin_time"`
	// EndTime 是音素结束时间(毫秒)。
	EndTime int `json:"end_time"`
}

Phoneme 是一个音素的时间戳信息,仅 Sambert 返回。

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool 在多个 CosyVoice / Qwen-Audio-TTS 合成任务之间复用 WebSocket 连接。

该 API 允许连接复用:任务收到 task-finished 后, 可以在同一条连接上用新的 task_id 发起下一个任务,省掉 TCP 握手、TLS 握手 与 WebSocket 升级的往返,显著降低首包延迟。

Pool 的入口是 NewSynthesisSession,语义与 Client.NewSynthesisSession 一致。 其余两套 API 直接用 Client 上的对应入口:Sambert 用 Client.NewSambertSession, Qwen-TTS Realtime 用 Client.NewRealtimeSession——后者的服务端在 session.finished 之后会主动关闭连接,本就无法复用。

Pool 从池中取出的会话与 Client 直接创建的会话是同一个 *Session 类型, 用法完全相同:Close 时由 Pool 决定是把连接交回池还是关闭它。

Pool 是并发安全的。

pool := voicecraftali.NewPool(client)
defer pool.Close()

session, err := pool.NewSynthesisSession(ctx, params)

func NewPool

func NewPool(client *Client, opts ...PoolOption) *Pool

NewPool 创建一个连接池。

client 必须非 nil。PoolOption 与 Client 的 Option 不同,非法取值不生效 而非报错——池的容量与空闲时长只影响性能,写错不会产生错误的合成结果。

Example

ExampleNewPool 展示连接复用:任务之间复用同一条 WebSocket 连接, 省掉握手往返以降低首包延迟。

package main

import (
	"context"
	"log"
	"os"
	"time"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	pool := voicecraftali.NewPool(client,
		voicecraftali.WithPoolMaxIdle(16),
		// 空闲上限需低于服务端 60 秒断连阈值。
		voicecraftali.WithPoolIdleTimeout(45*time.Second),
	)
	defer pool.Close()

	ctx := context.Background()
	params := &voicecraftali.SynthesisParams{
		Model: voicecraftali.ModelCosyVoiceV3Flash,
		Voice: "longanyang",
	}

	for _, text := range []string{"第一句。", "第二句。", "第三句。"} {
		session, err := pool.NewSynthesisSession(ctx, params)
		if err != nil {
			log.Fatal(err)
		}
		_ = session.SendText(ctx, text)
		_ = session.Finish(ctx)
		_ = session.Stream(ctx, func([]byte) error { return nil })
		// Close 把连接交回池,供下一个任务复用。
		_ = session.Close()
	}
}

func (*Pool) Close

func (p *Pool) Close() error

Close 关闭池中所有空闲连接。

Close 是幂等的。关闭后 NewSynthesisSession 返回 ErrPoolClosed; 此前已交出、仍在使用中的会话不受影响,它们关闭时连接会被直接关掉 而不是交回池。

func (*Pool) IdleCount

func (p *Pool) IdleCount() int

IdleCount 返回当前池中空闲连接的数量,用于监控与测试。

func (*Pool) NewSynthesisSession

func (p *Pool) NewSynthesisSession(
	ctx context.Context,
	params *SynthesisParams,
	opts ...SessionOption,
) (*Session, error)

NewSynthesisSession 从池中取一条连接创建 CosyVoice / Qwen-Audio-TTS 会话。

语义与 Client.NewSynthesisSession 完全一致,区别只在于连接可能来自池, 且会话正常结束后连接会被交回池。每个任务都会使用新生成的 task_id—— 复用连接时沿用旧 task_id 会被服务端拒绝。

type PoolOption

type PoolOption func(*Pool)

PoolOption 配置 Pool。

func WithPoolIdleTimeout

func WithPoolIdleTimeout(d time.Duration) PoolOption

WithPoolIdleTimeout 设置空闲连接的存活时长,默认 45 秒。

该值应当明显小于服务端的空闲断连阈值(60 秒),否则会取到已被服务端 关闭的连接。非正数不生效。

func WithPoolMaxIdle

func WithPoolMaxIdle(n int) PoolOption

WithPoolMaxIdle 设置空闲连接数上限,默认 8。 超出上限的连接会被关闭而不是无界堆积。非正数不生效。

type PreviewAudio

type PreviewAudio struct {
	// Data 是音频字节(本库已从 base64 解码)。
	Data []byte
	// SampleRate 是音频的实际采样率。
	SampleRate int
	// Format 是音频的实际格式。
	Format AudioFormat
}

PreviewAudio 是声音设计生成的试听音频。

type PreviewOptions

type PreviewOptions struct {
	// SampleRate 是预览音频采样率(Hz),零值表示不设置(服务端默认 24000)。
	// Qwen-Audio-TTS / CosyVoice 支持 16000、24000、48000;
	// Qwen 另外支持 8000。
	SampleRate int
	// ResponseFormat 是预览音频格式,零值表示不设置(服务端默认 wav)。
	// Qwen-Audio-TTS / CosyVoice 支持 pcm、wav、mp3;Qwen 另外支持 opus。
	ResponseFormat AudioFormat
}

PreviewOptions 是声音设计生成的预览音频的编码配置。

type Progress

type Progress struct {
	// Type 取 "sentence-begin"(句子开始)、"sentence-synthesis"(音频数据块)
	// 或 "sentence-end"(句子结束)。Sambert 不返回该字段。
	Type string
	// OriginalText 是分句后的句子文本,在 sentence-begin 与 sentence-end 中返回。
	// Sambert 不返回该字段。
	OriginalText string
	// Sentence 是句子与字、音素的时间戳信息,服务端未返回时为 nil。
	Sentence *Sentence
}

Progress 是两套 WebSocket 合成 API 在合成过程中返回的句子级进度信息。

通过 WithProgress 注册处理函数接收。开启时间戳参数后 Sentence 中会带有 时间戳数据。Qwen-TTS Realtime 没有等价的句子级进度事件,不会产生 Progress。

两套 API 填充的字段不同:

  • CosyVoice / Qwen-Audio-TTS:填 Type 与 OriginalText, Sentence.Index 有效,Words 带 BeginIndex / EndIndex
  • Sambert:不填 Type 与 OriginalText, Sentence.BeginTime / EndTime 有效,Words 可带 Phonemes

type Protocol

type Protocol string

Protocol 标识会话使用的 WebSocket 协议。

阿里云百炼的实时语音合成分三套 API,本库把它们收敛到同一个 Session 类型上, Protocol 用于在需要区分时查询当前会话属于哪一套。

const (
	// ProtocolSynthesis 是 CosyVoice / Qwen-Audio-TTS 的双向流式协议
	// (/api-ws/v1/inference,streaming=duplex)。
	ProtocolSynthesis Protocol = "synthesis"
	// ProtocolSambert 是 Sambert 的单向流式协议
	// (/api-ws/v1/inference,streaming=out)。与 ProtocolSynthesis 共用端点,
	// 但参数集与支持的指令都不同,因此是独立的协议标识。
	ProtocolSambert Protocol = "sambert"
	// ProtocolRealtime 是 Qwen-TTS Realtime 协议(/api-ws/v1/realtime),
	// 服务 qwen3-tts-*-realtime 与 qwen-tts-realtime 系列。
	ProtocolRealtime Protocol = "realtime"
)

type QwenTTSParams

type QwenTTSParams struct {
	// Model 是模型名,见 ModelQwen3TTSFlash 等常量。必填。
	Model string
	// Voice 是音色。必填。取值同 Qwen-TTS Realtime 的音色常量。
	Voice string

	// LanguageType 指定合成音频的语种,零值表示不设置(服务端默认 Auto)。
	LanguageType string

	// Instructions 是指令控制文本,长度不超过 1600 Token,仅支持中文与英文,
	// 仅 qwen3-tts-instruct-flash 系列支持。
	Instructions string
	// OptimizeInstructions 让服务端对 Instructions 做语义增强与重写。
	// 依赖 Instructions 非空。
	OptimizeInstructions bool
}

QwenTTSParams 是 Qwen-TTS 非实时 HTTP 合成的参数。

参数集比其他接口小得多:该接口只接受 text、voice、language_type、 instructions 与 optimize_instructions,没有音频格式、采样率、语速等控制项。

type QwenVoice

type QwenVoice struct {
	// Voice 是音色名,可直接用作 RealtimeParams.Voice 或 QwenTTSParams.Voice。
	Voice string
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string
	// FallbackMode 为 true 表示音色是以降级模式创建的:
	// 样本音频质量不佳或与文本严重不匹配,复刻效果可能不理想。
	FallbackMode bool
	// FallbackReason 是降级原因,仅在 FallbackMode 为 true 时有值。
	// 可能取值如 no_merged_segments(无法合并音频片段)、
	// no_valid_asr_segments(音频与文本严重不匹配)。
	FallbackReason string
}

QwenVoice 是一次 Qwen-TTS 声音复刻的结果。

type QwenVoiceAudio

type QwenVoiceAudio struct {
	// MIME 是音频的 MIME 类型,取值 audio/wav、audio/mpeg 或 audio/mp4。必填。
	MIME string
	// Data 是音频的原始字节。必填,且不超过 32 MiB——该上限是本库为
	// base64 编码与 JSON 序列化带来的内存放大所设,不是服务端的业务限制。
	// 本库会按协议要求把它编码成 Data URL。
	Data []byte
}

QwenVoiceAudio 是提交给 Qwen-TTS 声音复刻的样本音频。

type QwenVoiceList

type QwenVoiceList struct {
	// Voices 是本页音色。
	Voices []QwenVoiceSummary
	// PageIndex 是当前页码索引。
	PageIndex int
	// PageSize 是每页数据条数。
	PageSize int
	// TotalCount 是音色总数。
	TotalCount int
}

QwenVoiceList 是 Qwen-TTS 音色列表的分页结果。

type QwenVoiceSummary

type QwenVoiceSummary struct {
	// Voice 是音色名。
	Voice string `json:"voice"`
	// GmtCreate 是创建时间。
	GmtCreate string `json:"gmt_create"`
	// GmtModified 是修改时间。
	GmtModified string `json:"gmt_modified"`
	// Language 是样本音频的语种。
	Language string `json:"language"`
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string `json:"target_model"`
}

QwenVoiceSummary 是 Qwen-TTS 音色列表中的一项。

与 VoiceSummary 的差异:音色标识字段叫 voice 而非 voice_id, 带有 language 与 target_model,且**不含审核状态**。

type RealtimeMode

type RealtimeMode string

RealtimeMode 是 Qwen-TTS Realtime 的交互模式。

const (
	// ModeServerCommit 服务端自动判断文本分段与合成时机(协议默认)。
	// 平衡延迟与质量,适合大段文本连续合成。
	ModeServerCommit RealtimeMode = "server_commit"
	// ModeCommit 客户端调用 Commit 主动触发合成。
	// 延迟最低,但需要调用方自行保证句子完整性。
	ModeCommit RealtimeMode = "commit"
)

type RealtimeParams

type RealtimeParams struct {
	// Model 是模型名,见 ModelQwen3TTSFlashRealtime 等常量。必填。
	// 该值会作为 URL 查询参数 model 参与连接。
	Model string
	// Voice 是音色。必填。系统音色见 voice.go 中的常量与官方音色列表。
	Voice string

	// Mode 是交互模式,零值表示不设置(服务端默认 server_commit)。
	Mode RealtimeMode
	// LanguageType 指定合成音频的语种,零值表示不设置(服务端默认 Auto)。
	// 文本为单一语种时指定具体语种能显著提升合成质量。
	LanguageType string

	// ResponseFormat 是输出音频格式,零值表示不设置(服务端默认 pcm)。
	ResponseFormat AudioFormat
	// SampleRate 是输出采样率(Hz),取值 8000、16000、24000 或 48000,
	// 零值表示不设置(服务端默认 24000)。
	SampleRate int

	// SpeechRate 是语速,取值范围 [0.5, 2.0],零值表示不设置(服务端默认 1.0)。
	SpeechRate float64
	// Volume 是音量,取值范围 [0, 100],服务端默认 50。
	// 用指针的原因同 SynthesisParams.Volume。
	Volume *int
	// PitchRate 是语调,取值范围 [0.5, 2.0],零值表示不设置(服务端默认 1.0)。
	PitchRate float64
	// BitRate 是码率(kbps),取值范围 [6, 510],**仅 opus 格式有效**,
	// 设了别的格式(含 ResponseFormat 留零值,服务端此时默认 pcm)会被拒绝。
	// 零值表示不设置(服务端默认 128)。
	BitRate int

	// Instructions 是指令控制文本,长度不超过 1600 Token,仅支持中文与英文,
	// 仅 qwen3-tts-instruct-flash-realtime 系列支持。
	Instructions string
	// OptimizeInstructions 让服务端对 Instructions 做语义增强与重写。
	// 依赖 Instructions 非空,Instructions 为空时不生效。
	OptimizeInstructions bool
}

RealtimeParams 是 Qwen-TTS Realtime 的会话参数。

Model 与 Voice 必填,其余留零值即由服务端应用默认值。 qwen-tts-realtime 系列只支持 pcm 格式与 24000 采样率,且不支持 SpeechRate、Volume、PitchRate 与 BitRate;具体以官方 API 参考为准。

type Region

type Region string

Region 是阿里云百炼的服务地域。

两个地域的 API Key 不通用:用北京地域的 Key 调新加坡地域的端点会握手失败。

const (
	// RegionBeijing 华北2(北京)。
	RegionBeijing Region = "cn-beijing"
	// RegionSingapore 新加坡。
	RegionSingapore Region = "ap-southeast-1"
)

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry 持有多个具名 Client,用于在一个进程内同时使用多个 API Key。

Registry 在 NewRegistry 返回后不可变,且可以安全地在多个 goroutine 间共享 (safe for concurrent use):内部映射只读,因此不需要加锁。它不提供构造后 新增或替换条目的方法,与 Client 自身「New 返回后不可变」的契约一致;需要在 单个条目内轮转密钥的场景用 WithCredentialProvider。

用法:

reg, err := voicecraftali.NewRegistry(
    []voicecraftali.Option{voicecraftali.WithUserAgent("svc/1.0")},
    voicecraftali.RegistryEntry{
        Name:    "tenant-a",
        Options: []voicecraftali.Option{voicecraftali.WithAPIKey(keyA)},
    },
    voicecraftali.RegistryEntry{
        Name: "tenant-b",
        Options: []voicecraftali.Option{
            voicecraftali.WithAPIKey(keyB),
            voicecraftali.WithWorkspaceID("llm-b"),
            voicecraftali.WithDedicatedEndpoint(),
        },
    },
)
if err != nil {
    return err
}
client, err := reg.Client("tenant-b")

func NewRegistry

func NewRegistry(shared []Option, entries ...RegistryEntry) (*Registry, error)

NewRegistry 构造一个 Registry,为每个条目建立一个独立的 Client。

shared 中的选项会应用到每个条目,且在条目自身的选项之前应用,因此条目选项 覆盖同名的共享选项。

本函数不修改 shared 切片,也不写入它的底层数组,因此调用方可以放心传入某个 更大切片的前缀(如 all[:2]),并在 NewRegistry 返回后继续使用该数组。

进程级的公共配置放进 shared 即可,例如共用一个 http.Client 让全部条目复用 同一个连接池,或统一 User-Agent;需要独立连接池的条目在自己的 Options 里 再传一个 WithHTTPClient。

与 New 不同,条目必须通过 WithAPIKey 或 WithCredentialProvider 显式配置凭据, 不会回退到 DASHSCOPE_API_KEY 环境变量:注册表的用途就是区分多个 key,漏配 凭据的条目若静默继承环境里的那一个,它的全部调用会被计入错误的账号。漏配 的条目返回 ErrNoCredential。

以下输入返回错误:条目列表为空、条目名为空、条目名重复、选项为 nil、 条目选项校验失败。 重名是错误而不是「后者覆盖前者」——被覆盖的那个名字会指向另一个 API Key。 任一条目失败即返回 nil 注册表,不返回部分可用的结果;错误信息带上出错的 条目名,不含凭据值,且保留原始错误类型供 errors.Is 与 errors.As 判定。

Example

ExampleNewRegistry 展示一个进程内使用多个 API Key: 两个租户各自一份凭据,其中一个走业务空间专属域名。

package main

import (
	"fmt"
	"log"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	reg, err := voicecraftali.NewRegistry(
		// 共享选项应用到每个条目,条目自身的选项优先。
		[]voicecraftali.Option{voicecraftali.WithUserAgent("my-svc/1.0")},
		voicecraftali.RegistryEntry{
			Name:    "tenant-a",
			Options: []voicecraftali.Option{voicecraftali.WithAPIKey("sk-a")},
		},
		voicecraftali.RegistryEntry{
			Name: "tenant-b",
			Options: []voicecraftali.Option{
				voicecraftali.WithAPIKey("sk-b"),
				voicecraftali.WithWorkspaceID("llm-b"),
				voicecraftali.WithDedicatedEndpoint(),
			},
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	client, err := reg.Client("tenant-b")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reg.Names(), client != nil)
}
Output:
[tenant-a tenant-b] true

func (*Registry) Client

func (r *Registry) Client(name string) (*Client, error)

Client 返回 name 对应的 Client。

name 未注册时返回 nil 与包装了 ErrUnknownClient 的错误。

Example

ExampleRegistry_Client 展示取用未注册的名字。

package main

import (
	"errors"
	"fmt"
	"log"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	reg, err := voicecraftali.NewRegistry(nil,
		voicecraftali.RegistryEntry{
			Name:    "tenant-a",
			Options: []voicecraftali.Option{voicecraftali.WithAPIKey("sk-a")},
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	_, err = reg.Client("tenant-z")
	fmt.Println(errors.Is(err, voicecraftali.ErrUnknownClient))
}
Output:
true

func (*Registry) Names

func (r *Registry) Names() []string

Names 返回全部已注册的条目名,按字典序排序。

返回的是副本,调用方修改它不会影响注册表。该方法面向启动期自检与诊断, 不在音频热路径上。

type RegistryEntry

type RegistryEntry struct {
	Name    string
	Options []Option
}

RegistryEntry 是注册表中的一个条目:一个逻辑名加一组构造选项。

Options 就是 New 接受的那套 Option,因此每个条目都能独立配置凭据、业务空间、 专属域名与自定义基址——同一地域下不同 API Key 的端点差异正来自后三者。

Name 是调用方自定的逻辑名(租户名、业务线名等),只用于索引,不参与任何 请求内容。

type SambertParams

type SambertParams struct {
	// Model 是模型名,如 ModelSambertZhichu。必填。音色由模型名决定。
	Model string
	// Text 是待合成的全文。必填——Sambert 不支持增量送文本。
	Text string

	// Format 是输出音频格式,取值 pcm、wav 或 mp3(不支持 opus),
	// 零值表示不设置(服务端默认 wav)。
	Format AudioFormat
	// SampleRate 是输出采样率(Hz),取值 8000、16000、22050 或 24000,
	// 零值表示不设置(服务端默认 16000)。
	SampleRate int

	// Volume 是音量,取值范围 [0, 100],服务端默认 50。
	// 用指针的原因同 SynthesisParams.Volume。
	Volume *int
	// Rate 是语速,取值范围 [0.5, 2.0],服务端默认 1.0。零值表示不设置。
	Rate float64
	// Pitch 是语调,取值范围 [0.5, 2.0],服务端默认 1.0。零值表示不设置。
	Pitch float64

	// WordTimestampEnabled 启用字级别时间戳,所有 Sambert 模型均支持。
	WordTimestampEnabled bool
	// PhonemeTimestampEnabled 启用音素级别时间戳。
	// 需要同时开启 WordTimestampEnabled,否则本库拒绝该组合。
	PhonemeTimestampEnabled bool
}

SambertParams 是 Sambert(WebSocket 单向流式)的合成参数。

Sambert 与 CosyVoice / Qwen-Audio-TTS 是两套不同的 API,参数集不是子集关系:

  • **没有 Voice**:音色由模型名决定(如 sambert-zhichu-v1 是知厨)
  • Text 必填:全文在建立会话时随 run-task 一次性提交,不支持增量送文本
  • 支持 PhonemeTimestampEnabled,而 API 1 没有该参数
  • 没有 BitRate、Seed、LanguageHints、Instruction、EnableSSML、HotFix 与 AIGC 系列参数

Sambert 仅在北京地域可用,且是早期模型;新项目建议优先选用 CosyVoice 或 Qwen-Audio-TTS。

type Sentence

type Sentence struct {
	// Index 是句子编号,从 0 开始(CosyVoice / Qwen-Audio-TTS)。
	Index int `json:"index"`
	// BeginTime 是句子起始时间(毫秒,Sambert)。
	BeginTime int `json:"begin_time"`
	// EndTime 是句子结束时间(毫秒,Sambert)。
	EndTime int `json:"end_time"`
	// Words 是字级时间戳列表,需开启对应参数才有内容。
	Words []Word `json:"words"`
}

Sentence 是一个句子的进度与时间戳信息。

字段按所属 API 填充:Index 由 CosyVoice / Qwen-Audio-TTS 返回, BeginTime 与 EndTime 由 Sambert 返回,未返回的字段保持零值。

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session 是一次实时语音合成会话。

三套 WebSocket API(CosyVoice/Qwen-Audio-TTS、Sambert、Qwen-TTS Realtime) 共用本类型,方法集合与语义一致,因此切换 API 不需要改动读写代码。 各 API 确实存在的能力差异以 ErrUnsupportedOperation 显式暴露,不静默降级:

| 方法           | CosyVoice/Qwen-Audio-TTS | Sambert                  | Qwen-TTS Realtime        |
|---------------|--------------------------|--------------------------|--------------------------|
| SendText      | 支持                      | ErrUnsupportedOperation  | 支持                      |
| Commit        | ErrUnsupportedOperation  | ErrUnsupportedOperation  | 支持                      |
| Flush         | 支持                      | ErrUnsupportedOperation  | ErrUnsupportedOperation  |
| Finish        | 发送 finish-task          | 空操作(该 API 无此指令)  | 发送 session.finish       |
| Cancel        | 支持                      | ErrUnsupportedOperation  | ErrUnsupportedOperation  |
| CancelResponse| ErrUnsupportedOperation  | ErrUnsupportedOperation  | 支持                      |

Cancel 与 CancelResponse 的区别是终结性:Cancel 结束整个任务,之后不能再 SendText;CancelResponse 只取消当前响应,会话继续可用。

并发契约(safe for concurrent use,但含义有限):

  • 一个 goroutine 写(SendText / Commit / Flush / Finish / Cancel / CancelResponse)与另一个 goroutine 读(Read / ReadContext / Stream)可以并发进行。
  • 多个 goroutine 并发写是安全的,写入会被串行化。
  • 多个 goroutine 并发读会各自取走不同的音频帧,通常不是想要的效果; 音频应当由单个 goroutine 消费。
  • Close 可以在任意时刻、由任意 goroutine 并发调用,且是幂等的。

注意:串行化只保证安全,不提升吞吐——一个会话就是一条 WebSocket 连接。 需要并发合成请创建多个会话。

使用流程:

session, err := client.NewSynthesisSession(ctx, params)
if err != nil {
    return err
}
defer session.Close()

go func() {
    _ = session.SendText(ctx, "床前明月光,疑是地上霜。")
    _ = session.Finish(ctx)
}()

err = session.Stream(ctx, func(audio []byte) error {
    _, err := w.Write(audio)
    return err
})

调用方必须持续读取音频:音频缓冲写满后接收会阻塞,反压传导到服务端, 最终会触发服务端的空闲断连。

func (*Session) Cancel

func (s *Session) Cancel(ctx context.Context) error

Cancel 中断当前轮次合成。

仅 CosyVoice / Qwen-Audio-TTS 支持,且服务端侧的支持范围有限: 北京地域 Qwen-Audio-TTS 全系支持、CosyVoice 仅 v2 及以上支持; 新加坡地域 CosyVoice 不支持。 服务端会立即结束当前任务并返回结束事件。

Sambert 与 Qwen-TTS Realtime 返回 ErrUnsupportedOperation:前者不接受 finish-task 指令(取消依赖它),后者没有终结式取消——要取消当前响应而保留 会话,用 CancelResponse。

Cancel 与 Finish 一样是终结动作:调用后不能再 SendText, 仍应继续读取直到音频流结束。

func (*Session) CancelResponse

func (s *Session) CancelResponse(ctx context.Context) error

CancelResponse 请求服务端取消当前响应的合成。

仅 Qwen-TTS Realtime 支持(发出 response.cancel 事件);CosyVoice / Qwen-Audio-TTS 与 Sambert 返回 ErrUnsupportedOperation——前者的取消是终结式的, 见 Cancel。

CancelResponse 不是终结动作:它不把会话标记为已结束,调用后 SendText 不会 因此返回 ErrSessionFinished。服务端在收到该事件之后是否仍接受新文本由服务端 决定,本方法只负责把事件发出去。

Example

ExampleSession_CancelResponse 展示取消 Qwen-TTS Realtime 的当前响应。

CancelResponse 不终结会话:取消掉当前这段音频之后,还能继续送新的文本。 需要结束整个任务用 Finish。

package main

import (
	"context"
	"log"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New()
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	session, err := client.NewRealtimeSession(ctx, &voicecraftali.RealtimeParams{
		Model: voicecraftali.ModelQwen3TTSFlashRealtime,
		Voice: voicecraftali.VoiceCherry,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	if err := session.SendText(ctx, "这段不要了。"); err != nil {
		log.Fatal(err)
	}
	// 用户改主意了:取消当前响应,会话继续可用。
	if err := session.CancelResponse(ctx); err != nil {
		log.Fatal(err)
	}
	if err := session.SendText(ctx, "换成这段。"); err != nil {
		log.Fatal(err)
	}
	if err := session.Finish(ctx); err != nil {
		log.Fatal(err)
	}
}

func (*Session) Close

func (s *Session) Close() error

Close 关闭会话并释放资源。

Close 是幂等的,可由任意 goroutine 在任意时刻调用。返回时保证: 接收 goroutine 已退出,底层连接已关闭或已交回连接池,阻塞在 Read / ReadContext / Stream 上的调用方已被解除阻塞。

会话属于连接池且任务干净结束时,连接被交回池而不是关闭。

func (*Session) Commit

func (s *Session) Commit(ctx context.Context) error

Commit 把当前文本缓冲区立即提交合成。

仅 Qwen-TTS Realtime 支持。commit 模式下必须调用 Commit 才会触发合成; server_commit 模式下服务端自动判定时机,调用 Commit 表示立即合成已缓冲的文本。

两套 WebSocket 合成 API(CosyVoice / Qwen-Audio-TTS 与 Sambert)返回 ErrUnsupportedOperation:它们没有独立的提交动作。

func (*Session) Finish

func (s *Session) Finish(ctx context.Context) error

Finish 通知服务端文本已全部送完。

调用后不能再 SendText,但仍应继续 Read 或 Stream 直到音频流结束—— 服务端会在 Finish 之后继续返回剩余音频。

Finish 是幂等的:重复调用返回 nil 且不会重复发帧。

func (*Session) Flush

func (s *Session) Flush(ctx context.Context) error

Flush 让服务端立即合成已送入但尚未合成的文本,不结束任务。

仅 CosyVoice / Qwen-Audio-TTS 支持;Sambert 与 Qwen-TTS Realtime 返回 ErrUnsupportedOperation。后者用 Commit 达到同样目的。

Flush 不是终结动作:调用后 SendText、Finish 与再次 Flush 都仍然可用。 该指令不携带文本,因此既不计入 SendText 的累计字符额度,也不占用 SSML 模式下 「整个任务只允许送入一次文本」的名额。

Example

ExampleSession_Flush 展示用 Flush 压低首包延迟。

Flush 让服务端立即合成已送入但尚未合成的文本,不结束任务,因此调用后仍可 继续送文本。适合上游文本逐字到达(如 LLM 流式输出)、又想尽快听到声音的场景。

package main

import (
	"context"
	"log"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New()
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
		Model: voicecraftali.ModelCosyVoiceV3Flash,
		Voice: "longanyang",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	go func() {
		// 攒到一个完整句子就 Flush,不必等整段文本都送完。
		for _, line := range []string{"床前明月光,", "疑是地上霜。"} {
			if err := session.SendText(ctx, line); err != nil {
				log.Print(err)
				return
			}
			if err := session.Flush(ctx); err != nil {
				log.Print(err)
				return
			}
		}
		if err := session.Finish(ctx); err != nil {
			log.Print(err)
		}
	}()

	if err := session.Stream(ctx, func(audio []byte) error {
		_ = audio
		return nil
	}); err != nil {
		log.Fatal(err)
	}
}

func (*Session) ID

func (s *Session) ID() string

ID 返回服务端维度的会话标识:两套 WebSocket 合成 API 为 task_id, Qwen-TTS Realtime 为 session id。 排查问题、提交工单时需要提供该值。

func (*Session) Protocol

func (s *Session) Protocol() Protocol

Protocol 返回该会话使用的协议。

func (*Session) Read

func (s *Session) Read() ([]byte, error)

Read 读取下一帧音频。

这是拉模式接口,适合需要精细控制处理节奏的场景。

返回值:

  • (data, nil):成功读到一帧音频
  • (nil, io.EOF):音频流正常结束
  • (nil, err):服务端返回失败事件、连接异常或读超时

音频流因错误终止后,重复调用仍返回同一个错误,不会退化为 io.EOF。

Read 在等待时不响应 context 取消,需要该能力请用 ReadContext。

func (*Session) ReadContext

func (s *Session) ReadContext(ctx context.Context) ([]byte, error)

ReadContext 与 Read 相同,但在等待音频时响应 ctx 取消。

func (*Session) SendText

func (s *Session) SendText(ctx context.Context, text string) error

SendText 增量送入一段待合成文本,可多次调用。

文本送完后调用 Finish 通知服务端。

返回 ErrSessionFinished 表示已调用过 Finish;返回 ErrSessionClosed 表示会话已关闭; 返回 ErrTextTooLong 表示超出协议的单次或累计长度上限; 返回 ErrUnsupportedOperation 表示该 API 不支持增量送文本(Sambert 的全文 在创建会话时随 SambertParams.Text 一次提交)。

func (*Session) Stream

func (s *Session) Stream(ctx context.Context, handler func(audio []byte) error) error

Stream 以回调方式消费音频,直到音频流结束或 handler 返回错误。

这是推模式接口,适合「收到即转发」的流水线场景,例如直接写入 http.ResponseWriter 或 gRPC stream。

音频流正常结束时返回 nil;handler 返回错误时中止读取并返回包裹后的该错误; 服务端失败或连接异常时返回对应错误。

func (*Session) Usage

func (s *Session) Usage() *Usage

Usage 返回服务端返回的计费信息,尚未收到时返回 nil。

各 API 的计费口径不同,见 Usage 的字段说明。用量在合成过程中会持续更新, 应在音频流结束(Read 返回 io.EOF 或 Stream 返回 nil)之后再读取, 此时拿到的是最终值。

返回的是快照副本,调用方修改它不影响会话内部状态。

type SessionOption

type SessionOption func(*sessionConfig)

SessionOption 配置单个会话。

func WithProgress

func WithProgress(fn func(Progress)) SessionOption

WithProgress 注册合成进度回调,用于接收两套 WebSocket 合成 API (CosyVoice / Qwen-Audio-TTS 与 Sambert)的句子级进度与时间戳。

回调在会话的接收 goroutine 上同步执行,与音频投递共用同一个 goroutine: 回调阻塞会同时挡住音频投递,因此其中不要做耗时操作,需要时自行转交给 别的 goroutine。Qwen-TTS Realtime 没有等价事件,不会触发该回调。

时间戳数据需要同时开启 SynthesisParams.WordTimestampEnabled 或 PhonemeTimestampEnabled 才会返回。

Example

ExampleWithProgress 展示接收句子级进度与字级时间戳。

package main

import (
	"context"
	"log"
	"os"

	voicecraftali "github.com/gtkit/go-voicecraft-ali"
)

func main() {
	client, err := voicecraftali.New(voicecraftali.WithAPIKey(os.Getenv("DASHSCOPE_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	session, err := client.NewSynthesisSession(ctx, &voicecraftali.SynthesisParams{
		Model:                voicecraftali.ModelCosyVoiceV3Flash,
		Voice:                "longanyang",
		WordTimestampEnabled: true,
	}, voicecraftali.WithProgress(func(p voicecraftali.Progress) {
		// 回调在接收 goroutine 上同步执行,不要做耗时操作。
		if p.Type == "sentence-end" && p.Sentence != nil {
			for _, w := range p.Sentence.Words {
				log.Printf("%q %d-%dms", w.Text, w.BeginTime, w.EndTime)
			}
		}
	}))
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	_ = session.SendText(ctx, "床前明月光")
	_ = session.Finish(ctx)
	_ = session.Stream(ctx, func([]byte) error { return nil })
}

type SynthesisParams

type SynthesisParams struct {
	// Model 是模型名,见 ModelQwenAudioTTSFlash 等常量。必填。
	Model string
	// Voice 是音色。必填。系统音色见 voice.go 中的常量与官方音色列表,
	// 复刻/设计音色直接传其 ID。
	Voice string

	// Format 是输出音频格式,零值表示不设置。
	//
	// 不设置时服务端返回的是**裸 PCM**(16 bit 单声道,采样率随 SampleRate),
	// 不是 mp3——实测:同一段文本、cosyvoice-v3-flash,不传 Format 得到 87360 字节、
	// 头部 00 00 00 00;显式传 FormatMP3 得到 37531 字节、头部 49 44 33(ID3)。
	// 裸 PCM 没有容器头,调用方无法从数据本身得知采样率,也不能直接交给播放器。
	// 需要可直接播放的音频请显式指定 FormatMP3。
	Format AudioFormat
	// SampleRate 是输出采样率(Hz),取值 8000、16000、22050、24000、44100 或 48000,
	// 零值表示不设置(服务端默认 22050)。
	SampleRate int

	// Volume 是音量,取值范围 [0, 100],服务端默认 50。
	//
	// 用指针而非 int:音量 0 是合法取值,值语义无法把它与「不设置」区分开,
	// 会把「要求静音」误当成「用默认 50」。用 new(0) 明确表达前者。
	Volume *int

	// Rate 是语速,取值范围 [0.5, 2.0],服务端默认 1.0。零值表示不设置。
	// 对应线格式字段 rate(官方 SDK 中名为 speech_rate)。
	Rate float64
	// Pitch 是语调,取值范围 [0.5, 2.0],服务端默认 1.0。零值表示不设置。
	// 对应线格式字段 pitch(官方 SDK 中名为 pitch_rate)。
	Pitch float64
	// BitRate 是音频码率(kbps),取值范围 [6, 510],**仅 opus 格式有效**,
	// 设了别的格式(含 Format 留零值,此时服务端返回裸 PCM,同样不是 opus)会被拒绝。
	// 零值表示不设置(服务端默认 32)。cosyvoice-v1 不支持该参数。
	BitRate int
	// Seed 是随机数种子,取值范围 [0, 65535]。在模型版本、文本、音色及其他
	// 参数相同时,相同 Seed 可复现相同结果。零值即服务端默认值 0。
	// cosyvoice-v1 不支持该参数。
	Seed int

	// EnableSSML 启用 SSML 解析,此时 Text 需为 SSML 标记文本。
	//
	// 仅 CosyVoice 系列的部分模型支持(见 SupportsSSML),且音色需为复刻音色
	// 或官方列表中标注支持 SSML 的系统音色。用不支持的模型启用会在建立连接
	// 之前被拒绝。
	//
	// 启用后整个任务**只允许送入一次文本**:第二次 SendText 返回
	// ErrSSMLSingleSend,不会发往服务端。
	//
	// 拼接 SSML 时外部文本需先过 EscapeSSMLText 转义。
	EnableSSML bool
	// WordTimestampEnabled 启用字级别时间戳,时间戳随进度事件返回,
	// 通过 WithProgress 注册的回调接收。
	WordTimestampEnabled bool
	// EnableMarkdownFilter 在合成前过滤文本中的 Markdown 标记符号。
	// 仅 cosyvoice-v3-flash 的复刻音色支持。
	EnableMarkdownFilter bool

	// LanguageHints 指定目标语言以提升合成效果,取值为 zh、en、fr、de、ja、ko、
	// ru、pt、th、id、vi、es、it、ms、fil、ar。
	// 服务端当前只处理第一个元素,建议只传一个值。cosyvoice-v1 不支持该功能。
	LanguageHints []string
	// Instruction 是自然语言指令,用于控制方言、情感或角色等合成效果。
	Instruction string

	// EnableAIGCTag 在生成的音频中嵌入 AIGC 隐性标识(支持 wav/mp3/opus)。
	EnableAIGCTag bool
	// AIGCPropagator 设置 AIGC 隐性标识的 ContentPropagator 字段,
	// 仅在 EnableAIGCTag 为 true 时生效。
	AIGCPropagator string
	// AIGCPropagateID 设置 AIGC 隐性标识的 PropagateID 字段,
	// 仅在 EnableAIGCTag 为 true 时生效。
	AIGCPropagateID string

	// HotFix 是文本热修复配置。cosyvoice-v2 与 cosyvoice-v1 不支持。
	HotFix *HotFix
}

SynthesisParams 是 CosyVoice / Qwen-Audio-TTS(WebSocket 双向流式)的合成参数。

Model 与 Voice 必填,其余留零值即由服务端应用默认值。 各模型支持的参数子集不同(例如 cosyvoice-v1 不支持 Opus 格式、BitRate 与 Seed, cosyvoice-v2 与 v1 不支持 HotFix),具体以官方 API 参考为准; 本库只做取值域校验,不做模型能力矩阵校验。

Sambert 不使用本类型——它的参数集不同,见 SambertParams。

type SynthesisResult

type SynthesisResult struct {
	// RequestID 是本次调用的唯一标识符,排查问题时需要提供。
	RequestID string
	// Audio 是合成出的音频。
	Audio AudioRef
	// Usage 是本次调用的计费信息,服务端未返回时为 nil。
	Usage *Usage
}

SynthesisResult 是一次非实时 HTTP 合成的结果。

type TaskFailedError

type TaskFailedError struct {
	// TaskID 是本次任务的 task_id,提交工单排查时需要提供。
	TaskID string
	// Code 取服务端 header.error_code。可传给 CodeDescription 获取中文说明。
	Code Code
	// Message 取服务端 header.error_message,是服务端原始文案。
	Message string
}

TaskFailedError 表示两套 WebSocket 合成 API(CosyVoice / Qwen-Audio-TTS 与 Sambert)的服务端返回了 task-failed 事件。

服务端在发出该事件后会关闭连接,且该连接不可复用。

func (*TaskFailedError) Error

func (e *TaskFailedError) Error() string

type Usage

type Usage struct {
	// Characters 是计费字符数。
	Characters int `json:"characters,omitempty"`
	// TotalTokens 是输入与输出的总 Token 数。
	TotalTokens int `json:"total_tokens,omitempty"`
	// InputTokens 是输入 Token 数。
	InputTokens int `json:"input_tokens,omitempty"`
	// OutputTokens 是输出 Token 数。
	OutputTokens int `json:"output_tokens,omitempty"`
}

Usage 是服务端返回的计费信息。

各 API 的计费口径不同,未采用的口径保持零值:

  • CosyVoice / Qwen-Audio-TTS / Sambert / Qwen3-TTS Realtime:按字符计费(Characters)
  • Qwen-TTS Realtime:按 Token 计费(TotalTokens 等); 音频按每秒 50 Token 计,不足 1 秒按 50 Token 计

type ValidationError

type ValidationError struct {
	// Field 是出错的字段名,取协议线格式中的名字(如 "voice"、"sample_rate"),
	// 便于与官方文档对照。
	Field string
	// Reason 说明为什么该取值不合法。
	Reason string
}

ValidationError 表示一次本地参数校验失败,指明出错字段与原因。

本库在建立任何连接、发送任何帧之前完成校验,因此拿到 *ValidationError 意味着请求从未离开进程。

func (*ValidationError) Error

func (e *ValidationError) Error() string

func (*ValidationError) Is

func (e *ValidationError) Is(target error) bool

Is 让 *ValidationError 能被 errors.Is(err, ErrInvalidParam) 命中。

type VoiceDetail

type VoiceDetail struct {
	// ResourceLink 是复刻所用音频文件的地址,仅声音复刻的音色有值。
	ResourceLink string `json:"resource_link"`
	// TargetModel 是驱动该音色的语音合成模型。
	TargetModel string `json:"target_model"`
	// GmtCreate 是创建时间。
	GmtCreate string `json:"gmt_create"`
	// GmtModified 是修改时间。
	GmtModified string `json:"gmt_modified"`
	// Status 是审核状态。
	Status VoiceStatus `json:"status"`
	// VoicePrompt 是声音描述文本,仅声音设计的音色有值。
	VoicePrompt string `json:"voice_prompt"`
	// PreviewText 是预览文本,仅声音设计的音色有值。
	PreviewText string `json:"preview_text"`
}

VoiceDetail 是音色详情。

声音复刻与声音设计的音色共用本结构:复刻的音色有 ResourceLink(样本音频地址), 设计的音色有 VoicePrompt 与 PreviewText,两组字段互斥。

func (*VoiceDetail) Designed

func (v *VoiceDetail) Designed() bool

Designed 判断该音色是声音设计产出的(而非声音复刻)。

type VoiceStatus

type VoiceStatus string

VoiceStatus 是复刻音色的审核状态。

该状态体系仅适用于 Qwen-Audio-TTS / CosyVoice 的复刻音色; Qwen-TTS 的音色查询结果不含状态字段。

const (
	// VoiceStatusDeploying 审核中或处理中。
	VoiceStatusDeploying VoiceStatus = "DEPLOYING"
	// VoiceStatusOK 审核通过,可正常用于语音合成。
	VoiceStatusOK VoiceStatus = "OK"
	// VoiceStatusUndeployed 审核未通过,不可使用。
	VoiceStatusUndeployed VoiceStatus = "UNDEPLOYED"
)

func (VoiceStatus) Usable

func (s VoiceStatus) Usable() bool

Usable 判断该音色能否用于语音合成。

用复刻音色合成却听不到声音时,第一步就该确认状态是不是 OK。

type VoiceSummary

type VoiceSummary struct {
	// VoiceID 是音色 ID,可直接用作合成参数中的 Voice。
	VoiceID string `json:"voice_id"`
	// GmtCreate 是创建时间,形如 "2024-12-11 13:38:02"。
	GmtCreate string `json:"gmt_create"`
	// GmtModified 是修改时间。
	GmtModified string `json:"gmt_modified"`
	// Status 是审核状态,见 VoiceStatus。
	Status VoiceStatus `json:"status"`
	// VoicePrompt 是声音描述文本,仅声音设计的音色有值。
	VoicePrompt string `json:"voice_prompt"`
	// PreviewText 是预览文本,仅声音设计的音色有值。
	PreviewText string `json:"preview_text"`
}

VoiceSummary 是音色列表中的一项。

声音复刻与声音设计产出的音色共用同一份列表:VoicePrompt 与 PreviewText 只有声音设计的音色才有值,复刻出来的音色这两个字段为空。

func (*VoiceSummary) Designed

func (v *VoiceSummary) Designed() bool

Designed 判断该音色是声音设计产出的(而非声音复刻)。

type Word

type Word struct {
	// Text 是字的文本内容。
	Text string `json:"text"`
	// BeginIndex 是字在句子中的开始位置索引,从 0 开始(CosyVoice / Qwen-Audio-TTS)。
	BeginIndex int `json:"begin_index"`
	// EndIndex 是字在句子中的结束位置索引,从 1 开始(CosyVoice / Qwen-Audio-TTS)。
	EndIndex int `json:"end_index"`
	// BeginTime 是字对应音频的起始时间(毫秒)。
	BeginTime int `json:"begin_time"`
	// EndTime 是字对应音频的结束时间(毫秒)。
	EndTime int `json:"end_time"`
	// Phonemes 是音素级时间戳,仅 Sambert 在开启 PhonemeTimestampEnabled 后返回。
	Phonemes []Phoneme `json:"phonemes,omitempty"`
}

Word 是一个字的时间戳信息。

BeginIndex 与 EndIndex 由 CosyVoice / Qwen-Audio-TTS 返回; Phonemes 由 Sambert 在开启音素级时间戳后返回。未返回的字段保持零值。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL