Log bounded TTS failure categories for real calls

This commit is contained in:
2026-10-07 17:35:13 +08:00
parent 2146f79dbc
commit edf96f0645
3 changed files with 53 additions and 0 deletions
+31
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"os/exec"
"strconv"
"strings"
)
@@ -109,6 +110,36 @@ func synthesizeBailianTTS(ctx context.Context, approved TTSConfig, text string)
return pcm.Bytes(), nil
}
// bailianFailureSummary emits only fixed categories and HTTP status codes;
// provider responses, signed URLs, credentials and prompt text stay private.
func bailianFailureSummary(err error) (string, int) {
if err == nil {
return "none", 0
}
message := err.Error()
for _, item := range []struct{ prefix, kind string }{
{"Bailian TTS rejected request: HTTP ", "generation_http"},
{"Bailian audio download rejected: HTTP ", "audio_http"},
} {
if status, err := strconv.Atoi(strings.TrimPrefix(message, item.prefix)); strings.HasPrefix(message, item.prefix) && err == nil && status >= 100 && status <= 599 {
return item.kind, status
}
}
for _, item := range []struct{ prefix, kind string }{
{"Bailian TTS response has no complete audio reference", "missing_audio"},
{"Bailian TTS request failed:", "generation_transport"},
{"Bailian audio download failed:", "audio_transport"},
{"Bailian audio conversion failed:", "audio_conversion"},
{"Bailian TTS deadline or cancellation:", "deadline"},
{"Bailian TTS returned incomplete PCM16 audio", "incomplete_audio"},
} {
if strings.HasPrefix(message, item.prefix) {
return item.kind, 0
}
}
return "unknown", 0
}
type boundedPCM struct{ bytes.Buffer }
func (w *boundedPCM) Write(data []byte) (int, error) {
+18
View File
@@ -3,6 +3,7 @@ package ai
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os/exec"
@@ -50,6 +51,23 @@ func mockBailianTTS(t *testing.T, pcm []byte, observe func(string)) (string, *at
return server.URL + "/api/v1/services/aigc/multimodal-generation/generation", calls
}
func TestBailianFailureSummaryNeverLogsProviderPayload(t *testing.T) {
for _, tc := range []struct {
message, kind string
status int
}{
{"Bailian TTS rejected request: HTTP 403", "generation_http", 403},
{"Bailian audio download rejected: HTTP 502", "audio_http", 502},
{"Bailian TTS response has no complete audio reference", "missing_audio", 0},
{"Authorization: Bearer private-signature and prompt", "unknown", 0},
} {
kind, status := bailianFailureSummary(errors.New(tc.message))
if kind != tc.kind || status != tc.status || strings.Contains(kind, "private") {
t.Fatalf("unsafe failure summary: kind=%q status=%d", kind, status)
}
}
}
func TestBailianTTSDownloadFailureDoesNotLeakSignedURLOrRetry(t *testing.T) {
if _, err := exec.LookPath("ffmpeg"); err != nil {
t.Skip("Bailian TTS conversion requires ffmpeg")
+4
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"net/url"
"strings"
"sync"
@@ -55,6 +56,7 @@ func (c *Call) Open(ctx context.Context) ([]byte, error) {
}
audio, err := c.bound.Synthesize(ctx, c.bound.Opening)
if err != nil {
slog.Error("approved opening synthesis failed", "stage", "opening")
return nil, err
}
c.mu.Lock()
@@ -233,6 +235,8 @@ func (b Binding) synthesize(ctx context.Context, text string, alreadyPending int
// missing or failed response never counts as queued audio.
audio, err := synthesizeBailianTTS(ctx, *b.TTS, text)
if err != nil {
kind, status := bailianFailureSummary(err)
slog.Error("approved TTS synthesis failed", "category", kind, "http_status", status, "cause_type", fmt.Sprintf("%T", err))
return nil, 0, err
}
return audio, 1, nil