diff --git a/adapter/volcano/synthesis.go b/adapter/volcano/synthesis.go index 0c90237..1df6ffc 100644 --- a/adapter/volcano/synthesis.go +++ b/adapter/volcano/synthesis.go @@ -11,6 +11,7 @@ import ( "github.com/volcano-tts/tts-api/common" "github.com/volcano-tts/tts-api/dto" + "github.com/volcano-tts/tts-api/telemetry" ) // MetricsRecorder 是适配器向上报告埋点的接口。 @@ -83,7 +84,7 @@ func Synthesis( if common.DebugLog { log.Printf("TTS upstream: resource_id=%s speaker=%s model=%q format=%s sample_rate=%d speech_rate=%d additions=%q", - opts.ResourceID, opts.Speaker, opts.Model, opts.Format, opts.SampleRate, opts.SpeechRate, extractAdditionsForLog(body)) + opts.ResourceID, telemetry.MaskSpeaker(opts.Speaker), opts.Model, opts.Format, opts.SampleRate, opts.SpeechRate, extractAdditionsForLog(body)) } resp, err := client.PostStream(ctx, "https://openspeech.bytedance.com/api/v3/tts/unidirectional", headers, body) @@ -144,7 +145,7 @@ func Synthesis( mtr.UpstreamFinished(opts.Speaker, opts.Model, opts.Format, "ok", duration, parsed.FirstChunk, parsed.Chunks, len(finalData), 0) log.Printf("TTS 合成成功 - 音色=%s 格式=%s 文本=%d字 音频=%d字节 分片=%d 耗时=%v", - opts.Speaker, clientFormat, len(text), len(finalData), parsed.Chunks, duration) + telemetry.MaskSpeaker(opts.Speaker), clientFormat, len(text), len(finalData), parsed.Chunks, duration) return &dto.SynthesisResult{ AudioData: finalData, diff --git a/controller/tts.go b/controller/tts.go index 30248b5..e25a2b9 100644 --- a/controller/tts.go +++ b/controller/tts.go @@ -161,7 +161,10 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) { finalLabels := telemetry.Labels{ "format": clientFormat, - "speaker": opts.Speaker, + // speaker 是火山复刻音色 ID(用户付费资产),不能直接出现在 /metrics label 里 + //(无鉴权可枚举)。用 sha1[:8] 替代:同 speaker 同 label 保留 per-voice 观测, + //但反推不出原值。Admin UI 想要看原名通过 /api/voices 拿 name 字段。 + "speaker": telemetry.SpeakerLabel(opts.Speaker), "model": opts.Model, } if err != nil { diff --git a/setting/config.go b/setting/config.go index 405bf84..203388f 100644 --- a/setting/config.go +++ b/setting/config.go @@ -12,6 +12,7 @@ import ( "github.com/volcano-tts/tts-api/adapter/volcano" "github.com/volcano-tts/tts-api/common" + "github.com/volcano-tts/tts-api/telemetry" ) // 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。 @@ -318,7 +319,8 @@ func LogStartupSummary() { checks := []ttsCheck{ {"BYTEDANCE_TTS_API_KEY", maskAPIKey(TTSOptions.APIKey), TTSOptions.APIKey != ""}, {"BYTEDANCE_TTS_RESOURCE_ID", TTSOptions.ResourceID, TTSOptions.ResourceID != ""}, - {"BYTEDANCE_TTS_SPEAKER", TTSOptions.Speaker, TTSOptions.Speaker != ""}, + // speaker 是火山复刻音色 ID(用户付费资产),日志里打码,避免明文落盘 + {"BYTEDANCE_TTS_SPEAKER", telemetry.MaskSpeaker(TTSOptions.Speaker), TTSOptions.Speaker != ""}, } missingCount := 0 for _, c := range checks { diff --git a/telemetry/labels.go b/telemetry/labels.go index 91e818f..7dfaa29 100644 --- a/telemetry/labels.go +++ b/telemetry/labels.go @@ -8,11 +8,52 @@ // - Meter 是高层入口,NoopMeter 用于测试。 package telemetry -import "sort" +import ( + "crypto/sha1" + "encoding/hex" + "sort" + "strings" +) // Labels 是指标附加的标签集合。Value 在序列化时会按 Prometheus 规范转义。 type Labels map[string]string +// SpeakerLabel 把 speaker ID 转成不可逆的稳定短哈希,作为指标 label。 +// 目的:保护火山复刻音色资产(speaker ID 是用户付费 / 隐私敏感); +// 同时仍能按 voice 聚合观测(同 speaker → 同 label)。 +// +// 算法: sha1(s)[:8] = 32 bits 空间;典型 <100 个 voice 场景无碰撞风险。 +// 空串返回 "unknown",避免 /metrics label 出现空值 (Prometheus 禁止空 label)。 +// +// 注意: 这是**不可逆**哈希,不是加密;不可用于需要还原原始 speaker 的场景。 +// Admin UI 想要看原名时,通过 /api/voices 拿 name 字段对照。 +func SpeakerLabel(s string) string { + if s == "" { + return "unknown" + } + sum := sha1.Sum([]byte(s)) + return hex.EncodeToString(sum[:])[:8] +} + +// MaskSpeaker 把 speaker ID 部分打码用于日志输出。 +// - 空 → "***" +// - 长度 ≤ 4 → 全打码 +// - 其它 → 前 4 + **** + 后 4 (保留前缀便于肉眼区分 "S_xx 开头" vs "BV001_...") +// 例子: "S_G8tEKnaJ1" → "S_G8****naJ1" +func MaskSpeaker(s string) string { + if s == "" { + return "(未设置)" + } + if len(s) <= 4 { + return strings.Repeat("*", len(s)) + } + // 找前 4 字符中第一个非 [A-Za-z0-9_] 字符做截断,避免截到奇怪位置 + // (虽然火山 ID 实际都是 S_xxx 字母数字组合,这里保险) + prefix := s[:4] + suffix := s[len(s)-4:] + return prefix + "****" + suffix +} + // labelKey 计算一组标签的稳定 key,用于在内部 map 中唯一定位 child。 // 缺失或多余的 label 一律视为空串,以保证 child 数量与 label 名集合一致。 func labelKey(names []string, labels Labels) string {