- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息
⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
package voice
|
||
|
||
import (
|
||
"sort"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/transcode"
|
||
)
|
||
|
||
// TestPlayableIsFormatLevelVerdict 把「能不能播」的判据钉成**格式级**结论表(design/25
|
||
// §3.2 的 MDN 支持表)。AMR 是唯一必须转码的常见入站格式(Chrome/Firefox 的 AMR 支持为
|
||
// No,只有 ChromeOS 支持)——若有人把 amr 也标成可播,「能点开听」这条验收会静默失效
|
||
// (Web 上点开播不了,而服务端以为自己给了一份能播的副本)。
|
||
func TestPlayableIsFormatLevelVerdict(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
format string
|
||
want bool
|
||
why string
|
||
}{
|
||
{"wav", true, "<audio> 的基线格式"},
|
||
{"mp3", true, "KOOK 语音的实际格式"},
|
||
{"ogg", true, "Telegram 语音(Ogg/Opus)"},
|
||
{"oga", true, "Ogg 的同义扩展名"},
|
||
{"opus", true, "裸 Opus(按 Ogg 家族放行)"},
|
||
{"m4a", true, "MP4/AAC 家族"},
|
||
{"aac", true, "AAC 家族"},
|
||
{"flac", true, "无损容器"},
|
||
{"webm", true, "WebM/Opus"},
|
||
{"amr", false, "Chrome 的 AMR 支持为 No(唯一必需的转码路 amr→wav 的来源)"},
|
||
{"amr-wb", false, "同族不可播,且本期没有转码实现(后置 P2)"},
|
||
{"silk", false, "不在音频白名单内(微信按既有裁决不取字节)"},
|
||
{"", false, "空格式:deny-by-default"},
|
||
{"xyz", false, "不认识的格式:deny-by-default"},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.format+"/"+tc.why, func(t *testing.T) {
|
||
t.Parallel()
|
||
if got := Playable(tc.format); got != tc.want {
|
||
t.Fatalf("Playable(%q) = %v, want %v(%s)", tc.format, got, tc.want, tc.why)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestPlayableNormalizesFormatName 钉住归一:大小写、首尾空白、开头的点都要吃得下
|
||
// (渠道给的扩展名/MIME 归一后可能带点或大写),否则「可播」会在同一格式上给出两个答案。
|
||
func TestPlayableNormalizesFormatName(t *testing.T) {
|
||
t.Parallel()
|
||
for _, in := range []string{"WAV", " wav ", ".wav", ".WAV"} {
|
||
if !Playable(in) {
|
||
t.Fatalf("Playable(%q) = false, want true(归一后是 wav)", in)
|
||
}
|
||
}
|
||
for _, in := range []string{"AMR", " amr ", ".AMR"} {
|
||
if Playable(in) {
|
||
t.Fatalf("Playable(%q) = true, want false(归一后是 amr,不可播)", in)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestTranscodeTargetIsRegisteredOnlyForAMR 钉住「哪些不可播格式有登记的转码目标」:
|
||
// 本期只发布 `amr → wav`(design/25 §3.1)。可播格式**不是**转码目标(白转一圈没收益),
|
||
// silk 也**没有**目标(design/25 §3.3:不引入纯 Go SILK 解码器)。
|
||
func TestTranscodeTargetIsRegisteredOnlyForAMR(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
format string
|
||
want transcode.Format
|
||
ok bool
|
||
}{
|
||
{"amr", transcode.FormatWAV, true},
|
||
{"amr-wb", transcode.FormatWAV, true},
|
||
{"wav", "", false},
|
||
{"mp3", "", false},
|
||
{"silk", "", false},
|
||
{"", "", false},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.format, func(t *testing.T) {
|
||
t.Parallel()
|
||
got, ok := TranscodeTarget(tc.format)
|
||
if ok != tc.ok || got != tc.want {
|
||
t.Fatalf("TranscodeTarget(%q) = (%q, %v), want (%q, %v)",
|
||
tc.format, got, ok, tc.want, tc.ok)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestNeedsTranscodeDistinguishesCannotDoFromMustTranscode 钉住一条容易混的边界:
|
||
// NeedsTranscode 不是 `!Playable`。silk 不可播但**没登记目标** ⇒ false(做不了,
|
||
// 走 ErrNotPlayable),而不是「要转码」(那会去问一条注定不存在的路)。
|
||
func TestNeedsTranscodeDistinguishesCannotDoFromMustTranscode(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
format string
|
||
want bool
|
||
}{
|
||
{"amr", true},
|
||
{"amr-wb", true},
|
||
{"wav", false},
|
||
{"mp3", false},
|
||
{"silk", false},
|
||
{"", false},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.format, func(t *testing.T) {
|
||
t.Parallel()
|
||
if got := NeedsTranscode(tc.format); got != tc.want {
|
||
t.Fatalf("NeedsTranscode(%q) = %v, want %v", tc.format, got, tc.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestPlayableFormatsMatchesDesignTable 把「可播集合」的**字面成员**钉死(discovery 面
|
||
// 的作者容易顺手加一项而没有依据)。design/25 §3.2 的支持表 + design/24 §10.6 的音频
|
||
// 白名单(10 项扩展名)⇒ 可播名集合恰好是这 9 个(ogg/oga/opus 三名同容器)。
|
||
func TestPlayableFormatsMatchesDesignTable(t *testing.T) {
|
||
t.Parallel()
|
||
want := []string{"aac", "flac", "m4a", "mp3", "oga", "ogg", "opus", "wav", "webm"}
|
||
got := PlayableFormats()
|
||
sort.Strings(got)
|
||
if len(got) != len(want) {
|
||
t.Fatalf("PlayableFormats() = %v(%d 项), want %v(%d 项)", got, len(got), want, len(want))
|
||
}
|
||
for i := range want {
|
||
if got[i] != want[i] {
|
||
t.Fatalf("PlayableFormats() = %v, want %v", got, want)
|
||
}
|
||
}
|
||
}
|