Files
AskaEth ede00d9928 feat(voice): 语音消息落库 + 转码接入(切片 0020)
- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息

⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
2026-10-06 09:04:06 +08:00

134 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}