feat(voice): 语音silent降级信号(C2) — provider仅在结果为空时出现
- voice/Chain+Service: STTWithProvider/TTSWithProvider(真实档位cloud/local/edge-tts/espeak/silent), 旧方法改薄包装
- gateway: transcribe空文本带{text,provider} / voice_output空音频带provider / speech空音频tts_empty+X-Voice-Provider头
- middleware: CORS Access-Control-Expose-Headers: X-Voice-Provider
- 可选接口 voiceProviderReporter(type断言透传), 自定义实现回退silent
- 新增测试10个(provider_signal/voice_signal), build/vet/test全绿+交叉编译
- Docs/api-client v0.23 + api-reference CORS 同步
This commit is contained in:
1 parent
00f784b55f
commit
99f222761e
9 files changed
+345
-25
No files matched your search
@@ -28,6 +28,54 @@ type VoiceService interface {
|
||||
CloneVoice(ctx context.Context, referenceURL, prefix string) (string, error)
|
||||
}
|
||||
|
||||
// voiceProviderHeader is the response header carrying the provider rung on the
|
||||
// binary /api/v1/audio/speech response, where a JSON signal field cannot be added.
|
||||
const voiceProviderHeader = "X-Voice-Provider"
|
||||
|
||||
// voiceProviderReporter is the optional extension of VoiceService: a service that
|
||||
// can report which provider rung (cloud / local / edge-tts / espeak / silent)
|
||||
// produced a result. *voice.Chain and *voice.Service implement it; a custom
|
||||
// VoiceService that does not is still supported and falls back to the plain
|
||||
// STT/TTS methods with no reported rung.
|
||||
type voiceProviderReporter interface {
|
||||
STTWithProvider(ctx context.Context, audio voice.Audio) (string, string, error)
|
||||
TTSWithProvider(ctx context.Context, text string) (voice.Audio, string, error)
|
||||
}
|
||||
|
||||
// sttWithProvider runs STT and returns the transcript plus the provider rung that
|
||||
// served it ("" when the injected service cannot report one).
|
||||
func sttWithProvider(ctx context.Context, v VoiceService, audio voice.Audio) (string, string, error) {
|
||||
if r, ok := v.(voiceProviderReporter); ok {
|
||||
return r.STTWithProvider(ctx, audio)
|
||||
}
|
||||
text, err := v.STT(ctx, audio)
|
||||
return text, "", err
|
||||
}
|
||||
|
||||
// ttsWithProvider runs TTS and returns the audio plus the provider rung that
|
||||
// served it.
|
||||
func ttsWithProvider(ctx context.Context, v VoiceService, text string) (voice.Audio, string, error) {
|
||||
if r, ok := v.(voiceProviderReporter); ok {
|
||||
return r.TTSWithProvider(ctx, text)
|
||||
}
|
||||
audio, err := v.TTS(ctx, text)
|
||||
return audio, "", err
|
||||
}
|
||||
|
||||
// providerSignal returns the "provider" degraded signal for a voice result: the
|
||||
// actual provider rung when the result is empty, "silent" when the rung is
|
||||
// unknown, and "" (omit the field) when the result carries real text/audio. It
|
||||
// only flags "no real speech/audio"; it is never a business error.
|
||||
func providerSignal(rung string, empty bool) string {
|
||||
if !empty {
|
||||
return ""
|
||||
}
|
||||
if rung == "" {
|
||||
return "silent"
|
||||
}
|
||||
return rung
|
||||
}
|
||||
|
||||
// voiceInputRequest is the decoded payload of a voice_input frame. Audio is a
|
||||
// base64 (or data: URI) audio blob; Format is its encoding ("wav"/"mp3"/"ogg");
|
||||
// Text overrides STT when present (a client that already transcribed can skip
|
||||
@@ -119,18 +167,22 @@ func (h *hub) runVoiceTurn(c *wsConn, req voiceInputRequest) {
|
||||
if strings.TrimSpace(reply) == "" {
|
||||
reply = res.StopReason
|
||||
}
|
||||
audi, err := h.voice.TTS(ctx, reply)
|
||||
audi, rung, err := ttsWithProvider(ctx, h.voice, reply)
|
||||
if err != nil {
|
||||
h.log.Warn("voice tts", "err", err)
|
||||
h.voiceError(c, "voice synthesis failed")
|
||||
return
|
||||
}
|
||||
c.enqueueJSON(wsEvent{Type: wsVoiceOutput, Data: map[string]any{
|
||||
data := map[string]any{
|
||||
"audio": base64.StdEncoding.EncodeToString(audi.Data),
|
||||
"format": audi.Format,
|
||||
"text": transcript,
|
||||
"reply": reply,
|
||||
}})
|
||||
}
|
||||
if sig := providerSignal(rung, len(audi.Data) == 0); sig != "" {
|
||||
data["provider"] = sig
|
||||
}
|
||||
c.enqueueJSON(wsEvent{Type: wsVoiceOutput, Data: data})
|
||||
}
|
||||
|
||||
// voiceError replies with a typed error frame for a failed voice turn.
|
||||
|
||||
Reference in new issue
Block a user