docs(comments): 注释中文化 · internal/runtime/multimodal(21 文件,纯替换)
- 只翻注释/文档字符串,代码零变化:go/scanner 词法流比对 21 文件 token+字面量全等,增删行数 1:1(467/467) - 刻意保留非散文英文:JSON Schema description、工具 Description() 返回值、adapter prompt、API 路径与命令示例、环境变量名 - 逐行确认代码前缀未变,未触碰其他模块
This commit is contained in:
1 parent
b9e042d07d
commit
b0fbb8bc0d
21 files changed
+467
-467
No files matched your search
@@ -14,37 +14,37 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
)
|
||||
|
||||
// DashScope (Alibaba Bailian) STT/TTS adapters for the cloud rung of the voice
|
||||
// chain. Requests follow the DashScope public API:
|
||||
// 语音链云端一级的 DashScope(阿里百炼)STT/TTS 适配器。请求
|
||||
// 遵循 DashScope 公开 API:
|
||||
//
|
||||
// - STT: POST {base}/services/aigc/multimodal-generation/generation
|
||||
// (qwen-audio-3.0-asr-flash, base64 input audio)
|
||||
// (qwen-audio-3.0-asr-flash,base64 输入音频)
|
||||
// - TTS: POST {base}/services/audio/tts/SpeechSynthesizer
|
||||
// (cosyvoice-v3-flash, synthesized by voice_id)
|
||||
// (cosyvoice-v3-flash,按 voice_id 合成)
|
||||
//
|
||||
// Both take a non-streaming response here: STT returns the transcript in the
|
||||
// output envelope; TTS (no X-DashScope-SSE header) returns a 24h audio URL that
|
||||
// we download so the caller always gets the actual bytes. Audio format ↔ mime
|
||||
// is mapped in both directions (mimeOf / the data: URI in the STT request).
|
||||
// 两者在这里都取非流式响应:STT 从 output envelope 中返回
|
||||
// 转写;TTS(不带 X-DashScope-SSE 头)返回一个 24h 音频 URL,我们
|
||||
// 把它下载下来,使调用方总能拿到真实字节。音频格式 ↔ mime
|
||||
// 双向映射(mimeOf / STT 请求中的 data: URI)。
|
||||
|
||||
const (
|
||||
// defaultSTTSampleRate is the input audio sample rate the ASR expects.
|
||||
// defaultSTTSampleRate 是 ASR 期望的输入音频采样率。
|
||||
defaultSTTSampleRate = 16000
|
||||
// dashscopeSTTPath is the multimodal-generation service path (ASR).
|
||||
// dashscopeSTTPath 是 multimodal-generation 服务路径(ASR)。
|
||||
dashscopeSTTPath = "/services/aigc/multimodal-generation/generation"
|
||||
// dashscopeTTSPath is the SpeechSynthesizer service path (TTS).
|
||||
// dashscopeTTSPath 是 SpeechSynthesizer 服务路径(TTS)。
|
||||
dashscopeTTSPath = "/services/audio/tts/SpeechSynthesizer"
|
||||
// dashscopeClonePath is the voice-customization (声音复刻 create_voice) path.
|
||||
// dashscopeClonePath 是音色定制(声音复刻 create_voice)路径。
|
||||
dashscopeClonePath = "/services/audio/tts/customization"
|
||||
// defaultDashscopeBaseURL is the Bailian API root when none is configured.
|
||||
// defaultDashscopeBaseURL 是未配置时使用的百炼 API 根地址。
|
||||
defaultDashscopeBaseURL = "https://dashscope.aliyuncs.com/api/v1"
|
||||
// defaultClonePrefix is the voice-name prefix when none is supplied.
|
||||
// defaultClonePrefix 是未提供时的音色名前缀。
|
||||
defaultClonePrefix = "cyrene"
|
||||
// voiceEnrollmentModel is the DashScope voice-enrollment model ("create_voice").
|
||||
// voiceEnrollmentModel 是 DashScope 音色注册模型("create_voice")。
|
||||
voiceEnrollmentModel = "voice-enrollment"
|
||||
)
|
||||
|
||||
// DashscopeSTT transcribes audio via DashScope multimodal-generation.
|
||||
// DashscopeSTT 经 DashScope multimodal-generation 转写音频。
|
||||
type DashscopeSTT struct {
|
||||
endpoint string
|
||||
apiKey string
|
||||
@@ -55,8 +55,8 @@ type DashscopeSTT struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewDashscopeSTT builds the DashScope STT adapter from cfg. Missing model /
|
||||
// language / base URL fall back to the DashScope defaults.
|
||||
// NewDashscopeSTT 从 cfg 构造 DashScope STT 适配器。缺失的 model /
|
||||
// language / base URL 回退到 DashScope 默认值。
|
||||
func NewDashscopeSTT(cfg config.VoiceConfig) *DashscopeSTT {
|
||||
model := firstNonEmpty(cfg.STTModel, "qwen-audio-3.0-asr-flash")
|
||||
language := firstNonEmpty(cfg.STTLanguage, "zh")
|
||||
@@ -71,7 +71,7 @@ func NewDashscopeSTT(cfg config.VoiceConfig) *DashscopeSTT {
|
||||
}
|
||||
}
|
||||
|
||||
// STT posts the audio as a base64 data: URI and returns the recognized text.
|
||||
// STT 把音频作为 base64 data: URI 发送并返回识别出的文字。
|
||||
func (s *DashscopeSTT) STT(ctx context.Context, audio Audio) (string, error) {
|
||||
if s.apiKey == "" {
|
||||
return "", ErrNotConfigured
|
||||
@@ -119,7 +119,7 @@ func (s *DashscopeSTT) STT(ctx context.Context, audio Audio) (string, error) {
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// DashscopeTTS synthesizes speech video via DashScope SpeechSynthesizer.
|
||||
// DashscopeTTS 经 DashScope SpeechSynthesizer 合成语音。
|
||||
type DashscopeTTS struct {
|
||||
endpoint string
|
||||
apiKey string
|
||||
@@ -131,8 +131,8 @@ type DashscopeTTS struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewDashscopeTTS builds the DashScope TTS adapter from cfg. Missing model /
|
||||
// voice / format / base URL fall back to the DashScope defaults.
|
||||
// NewDashscopeTTS 从 cfg 构造 DashScope TTS 适配器。缺失的 model /
|
||||
// voice / format / base URL 回退到 DashScope 默认值。
|
||||
func NewDashscopeTTS(cfg config.VoiceConfig) *DashscopeTTS {
|
||||
model := firstNonEmpty(cfg.TTSModel, "cosyvoice-v3-flash")
|
||||
voiceID := firstNonEmpty(cfg.TTSVoiceID, "longxiaochun_v3")
|
||||
@@ -153,9 +153,9 @@ func NewDashscopeTTS(cfg config.VoiceConfig) *DashscopeTTS {
|
||||
}
|
||||
}
|
||||
|
||||
// TTS synthesizes audio for text. The non-streaming DashScope response carries a
|
||||
// 24h audio URL (or a base64 blob); we resolve it to bytes and return them so the
|
||||
// caller holds a complete audio file.
|
||||
// TTS 为文字合成音频。非流式的 DashScope 响应携带一个
|
||||
// 24h 音频 URL(或一段 base64);我们把它解析成字节并返回,
|
||||
// 使调用方拿到完整的音频文件。
|
||||
func (t *DashscopeTTS) TTS(ctx context.Context, text string) (Audio, error) {
|
||||
if t.apiKey == "" {
|
||||
return Audio{}, ErrNotConfigured
|
||||
@@ -182,7 +182,7 @@ func (t *DashscopeTTS) TTS(ctx context.Context, text string) (Audio, error) {
|
||||
if err != nil {
|
||||
return Audio{}, fmt.Errorf("voice: %w: marshal: %v", ErrProviderFailed, err)
|
||||
}
|
||||
// No X-DashScope-SSE header → non-streaming URL response.
|
||||
// 不带 X-DashScope-SSE 头 → 返回非流式 URL 响应。
|
||||
body, err := dashscopePost(ctx, t.client, t.apiKey, t.endpoint, payload, "")
|
||||
if err != nil {
|
||||
return Audio{}, err
|
||||
@@ -194,11 +194,11 @@ func (t *DashscopeTTS) TTS(ctx context.Context, text string) (Audio, error) {
|
||||
return Audio{Format: t.format, Data: data}, nil
|
||||
}
|
||||
|
||||
// EffectiveInstruction returns the instruction to send for this synthesis: a
|
||||
// per-request override threaded through ctx wins, else the configured default. An
|
||||
// empty result means the request carries no instruction. A system voice never gets
|
||||
// a free-form instruction (see canInstruct), because cosyvoice v3 system voices
|
||||
// require a fixed instruction format.
|
||||
// EffectiveInstruction 返回本次合成要发送的 instruction:经 ctx 透传的
|
||||
// 逐请求覆盖优先,否则用配置的默认值。结果为空表示该请求
|
||||
// 不携带 instruction。系统音色永远不会收到自由形式的 instruction
|
||||
// (见 canInstruct),因为 cosyvoice v3 系统音色
|
||||
// 要求固定的 instruction 格式。
|
||||
func (t *DashscopeTTS) EffectiveInstruction(ctx context.Context) string {
|
||||
if !canInstruct(t.voiceID) {
|
||||
return ""
|
||||
@@ -209,16 +209,16 @@ func (t *DashscopeTTS) EffectiveInstruction(ctx context.Context) string {
|
||||
return t.instruction
|
||||
}
|
||||
|
||||
// canInstruct reports whether voiceID accepts a free-form instruction. cosyvoice v3
|
||||
// system voices (the long*_v3 family, e.g. longxiaochun_v3) require a fixed
|
||||
// instruction format; a cloned/designed voice (声音复刻, whose id never ends in
|
||||
// "_v3") accepts an arbitrary instruction.
|
||||
// canInstruct 报告 voiceID 是否接受自由形式的 instruction。cosyvoice v3
|
||||
// 系统音色(long*_v3 系列,例如 longxiaochun_v3)要求固定的
|
||||
// instruction 格式;复刻/设计音色(声音复刻,其 id 永不以
|
||||
// "_v3" 结尾)接受任意 instruction。
|
||||
func canInstruct(voiceID string) bool {
|
||||
return !strings.HasSuffix(voiceID, "_v3")
|
||||
}
|
||||
|
||||
// audioBytes resolves the synthesized audio from the DashScope response: a 24h
|
||||
// URL is downloaded, or an inline base64 blob is decoded.
|
||||
// audioBytes 从 DashScope 响应中解析合成音频:24h
|
||||
// URL 会被下载,内联的 base64 块会被解码。
|
||||
func (t *DashscopeTTS) audioBytes(ctx context.Context, body []byte) ([]byte, error) {
|
||||
var r dashscopeTTSResponse
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
@@ -241,8 +241,8 @@ func (t *DashscopeTTS) audioBytes(ctx context.Context, body []byte) ([]byte, err
|
||||
return nil, fmt.Errorf("voice: %w: no audio in response", ErrProviderFailed)
|
||||
}
|
||||
|
||||
// download fetches an audio URL into memory, capping the size so an oversized
|
||||
// media response cannot exhaust memory.
|
||||
// download 把音频 URL 抓进内存,并限制体积,使过大的
|
||||
// 媒体响应无法耗尽内存。
|
||||
func (t *DashscopeTTS) download(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -263,10 +263,10 @@ func (t *DashscopeTTS) download(ctx context.Context, url string) ([]byte, error)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// DashscopeCloner creates custom TTS voices via the DashScope voice-enrollment
|
||||
// API (声音复刻 create_voice). The reference audio must be a public URL — the
|
||||
// create_voice action only accepts input.url (no base64). The returned voice_id is
|
||||
// bound to TTSModel, so target_model must match the model used at synthesis time.
|
||||
// DashscopeCloner 经 DashScope 音色注册 API
|
||||
// (声音复刻 create_voice)创建自定义 TTS 音色。参考音频必须是公开
|
||||
// URL —— create_voice 动作只接受 input.url(不支持 base64)。返回的 voice_id
|
||||
// 绑定到 TTSModel,因此 target_model 必须与合成时使用的模型一致。
|
||||
type DashscopeCloner struct {
|
||||
endpoint string
|
||||
apiKey string
|
||||
@@ -274,9 +274,9 @@ type DashscopeCloner struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewDashscopeCloner builds the DashScope voice cloner from cfg. Missing model /
|
||||
// base URL fall back to the DashScope defaults; the model tracks TTSModel so the
|
||||
// cloned voice is usable by the TTS rung (target_model must match).
|
||||
// NewDashscopeCloner 从 cfg 构造 DashScope 音色复刻器。缺失的 model /
|
||||
// base URL 回退到 DashScope 默认值;model 跟随 TTSModel,使
|
||||
// 复刻出的音色可被 TTS 一级使用(target_model 必须一致)。
|
||||
func NewDashscopeCloner(cfg config.VoiceConfig) *DashscopeCloner {
|
||||
return &DashscopeCloner{
|
||||
endpoint: dashscopeEndpoint(cfg.DashscopeBaseURL) + dashscopeClonePath,
|
||||
@@ -286,9 +286,9 @@ func NewDashscopeCloner(cfg config.VoiceConfig) *DashscopeCloner {
|
||||
}
|
||||
}
|
||||
|
||||
// CloneVoice submits a create_voice request and returns the created voice_id. It
|
||||
// is the adapter behind the REST /api/v1/audio/voice-clone endpoint. A missing API
|
||||
// key returns ErrNotConfigured; an empty URL or an invalid prefix is rejected.
|
||||
// CloneVoice 提交一次 create_voice 请求并返回创建出的 voice_id。它
|
||||
// 是 REST /api/v1/audio/voice-clone endpoint 背后的适配器。缺少 API
|
||||
// key 时返回 ErrNotConfigured;空 URL 或非法 prefix 会被拒绝。
|
||||
func (d *DashscopeCloner) CloneVoice(ctx context.Context, referenceURL, prefix string) (string, error) {
|
||||
if d.apiKey == "" {
|
||||
return "", ErrNotConfigured
|
||||
@@ -333,9 +333,9 @@ func (d *DashscopeCloner) CloneVoice(ctx context.Context, referenceURL, prefix s
|
||||
return resp.Output.VoiceID, nil
|
||||
}
|
||||
|
||||
// normalizeClonePrefix trims and validates the voice-name prefix. An empty value
|
||||
// falls back to defaultClonePrefix; the result must be ≤10 letter/digit chars
|
||||
// (the voice-enrollment prefix constraint).
|
||||
// normalizeClonePrefix 去空白并校验音色名前缀。空值
|
||||
// 回退到 defaultClonePrefix;结果必须是 ≤10 个字母/数字字符
|
||||
// (音色注册对 prefix 的约束)。
|
||||
func normalizeClonePrefix(prefix string) (string, error) {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
@@ -352,8 +352,8 @@ func normalizeClonePrefix(prefix string) (string, error) {
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
// dashscopeEndpoint returns the DashScope API base with a trailing slash trimmed,
|
||||
// defaulting to the public Bailian root when none is configured.
|
||||
// dashscopeEndpoint 返回去掉尾部斜杠的 DashScope API 根地址,
|
||||
// 未配置时默认为公开的百炼根地址。
|
||||
func dashscopeEndpoint(baseURL string) string {
|
||||
base := strings.TrimRight(baseURL, "/")
|
||||
if base == "" {
|
||||
@@ -362,7 +362,7 @@ func dashscopeEndpoint(baseURL string) string {
|
||||
return base
|
||||
}
|
||||
|
||||
// firstNonEmpty returns a when non-empty, else b.
|
||||
// firstNonEmpty 在 a 非空时返回 a,否则返回 b。
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
@@ -370,9 +370,9 @@ func firstNonEmpty(a, b string) string {
|
||||
return b
|
||||
}
|
||||
|
||||
// dashscopePost POSTs a JSON payload with the DashScope bearer auth and an
|
||||
// optional X-DashScope-SSE header, returning the decoded response body. A non-200
|
||||
// status is wrapped with the DashScope error body.
|
||||
// dashscopePost POST 一个 JSON 载荷,带上 DashScope bearer 鉴权与可选的
|
||||
// X-DashScope-SSE 头,返回解码后的响应体。非 200
|
||||
// 状态会被包装上 DashScope 的错误体。
|
||||
func dashscopePost(ctx context.Context, client *http.Client, apiKey, url string, payload []byte, sse string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
@@ -400,8 +400,8 @@ func dashscopePost(ctx context.Context, client *http.Client, apiKey, url string,
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// extractSTTText pulls the transcript out of a DashScope ASR response. The ASR
|
||||
// output shape varies by model, so several candidate paths are tried.
|
||||
// extractSTTText 从 DashScope ASR 响应中取出转写。ASR
|
||||
// 的输出形状随模型而异,因此按多个候选路径依次尝试。
|
||||
func extractSTTText(body []byte) (string, error) {
|
||||
var r dashscopeSTTResponse
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
@@ -426,7 +426,7 @@ func extractSTTText(body []byte) (string, error) {
|
||||
return r.Text, nil
|
||||
}
|
||||
|
||||
// mimeOf maps an audio format to its MIME type for the data: URI / Content-Type.
|
||||
// mimeOf 把音频格式映射为其 MIME 类型,供 data: URI / Content-Type 使用。
|
||||
func mimeOf(format string) string {
|
||||
switch strings.ToLower(format) {
|
||||
case "mp3", "mpeg":
|
||||
@@ -440,7 +440,7 @@ func mimeOf(format string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// dashscopeSTTResponse is the DashScope ASR response envelope.
|
||||
// dashscopeSTTResponse 是 DashScope ASR 响应 envelope。
|
||||
type dashscopeSTTResponse struct {
|
||||
Output struct {
|
||||
Output struct {
|
||||
@@ -461,7 +461,7 @@ type dashscopeSTTResponse struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// dashscopeTTSResponse is the DashScope TTS response envelope.
|
||||
// dashscopeTTSResponse 是 DashScope TTS 响应 envelope。
|
||||
type dashscopeTTSResponse struct {
|
||||
Output struct {
|
||||
Audio struct {
|
||||
@@ -471,66 +471,66 @@ type dashscopeTTSResponse struct {
|
||||
} `json:"output"`
|
||||
}
|
||||
|
||||
// dashscopeSTTRequest is the ASR request body.
|
||||
// dashscopeSTTRequest 是 ASR 请求体。
|
||||
type dashscopeSTTRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input dashscopeSTTInput `json:"input"`
|
||||
Parameters dashscopeSTTParameters `json:"parameters"`
|
||||
}
|
||||
|
||||
// dashscopeSTTInput carries the multimodal messages list.
|
||||
// dashscopeSTTInput 承载多模态消息列表。
|
||||
type dashscopeSTTInput struct {
|
||||
Messages []dashscopeSTTMessage `json:"messages"`
|
||||
}
|
||||
|
||||
// dashscopeSTTMessage is one ASR user turn.
|
||||
// dashscopeSTTMessage 是一个 ASR 用户回合。
|
||||
type dashscopeSTTMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content []dashscopeSTTContent `json:"content"`
|
||||
}
|
||||
|
||||
// dashscopeSTTContent is one audio content block.
|
||||
// dashscopeSTTContent 是一个音频内容块。
|
||||
type dashscopeSTTContent struct {
|
||||
Type string `json:"type"`
|
||||
InputAudio dashscopeInputAudio `json:"input_audio"`
|
||||
}
|
||||
|
||||
// dashscopeInputAudio carries the base64 data: URI audio.
|
||||
// dashscopeInputAudio 承载 base64 data: URI 音频。
|
||||
type dashscopeInputAudio struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// dashscopeSTTParameters tunes the ASR request.
|
||||
// dashscopeSTTParameters 调整 ASR 请求。
|
||||
type dashscopeSTTParameters struct {
|
||||
Format string `json:"format"`
|
||||
SampleRate int `json:"sample_rate"`
|
||||
LanguageHints []string `json:"language_hints,omitempty"`
|
||||
}
|
||||
|
||||
// dashscopeTTSRequest is the TTS request body.
|
||||
// dashscopeTTSRequest 是 TTS 请求体。
|
||||
type dashscopeTTSRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input dashscopeTTSInput `json:"input"`
|
||||
}
|
||||
|
||||
// dashscopeTTSInput carries text + voice + output format.
|
||||
// dashscopeTTSInput 承载文字 + 音色 + 输出格式。
|
||||
type dashscopeTTSInput struct {
|
||||
Text string `json:"text"`
|
||||
Voice string `json:"voice"`
|
||||
Format string `json:"format"`
|
||||
SampleRate int `json:"sample_rate"`
|
||||
// Instruction is the CosyVoice emotion/tone instruction; omitempty keeps a
|
||||
// system voice (which must use a fixed instruction) free of a free-form one.
|
||||
// Instruction 是 CosyVoice 的情绪/语气 instruction;omitempty 使
|
||||
// 系统音色(必须使用固定 instruction)不携带自由形式的 instruction。
|
||||
Instruction string `json:"instruction,omitempty"`
|
||||
}
|
||||
|
||||
// dashscopeCloneRequest is the voice-enrollment create_voice request body.
|
||||
// dashscopeCloneRequest 是音色注册 create_voice 请求体。
|
||||
type dashscopeCloneRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input dashscopeCloneInput `json:"input"`
|
||||
}
|
||||
|
||||
// dashscopeCloneInput carries the create_voice action parameters.
|
||||
// dashscopeCloneInput 承载 create_voice 动作参数。
|
||||
type dashscopeCloneInput struct {
|
||||
Action string `json:"action"`
|
||||
TargetModel string `json:"target_model"`
|
||||
@@ -538,8 +538,8 @@ type dashscopeCloneInput struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// dashscopeCloneResponse is the voice-enrollment response envelope. A top-level
|
||||
// message marks a model-level error surfaced even on an HTTP 200.
|
||||
// dashscopeCloneResponse 是音色注册响应 envelope。顶层
|
||||
// message 表示模型级错误——即使 HTTP 200 也会暴露。
|
||||
type dashscopeCloneResponse struct {
|
||||
Output struct {
|
||||
VoiceID string `json:"voice_id"`
|
||||
|
||||
Reference in new issue
Block a user