feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 13:02:28 +08:00
1 parent 89715d1813
commit 4a2d276881
100 files changed
+11319

No files matched your search

+262
View File
@@ -0,0 +1,262 @@
package transcode
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
)
// Engine 是 Transcoder 的默认实现:把「格式无关」的那一层(输入字节上限、
// 并发信号量、单次超时、日志)收在一处,把「格式相关」的那一层交给注册进来的
// Codec(design/25 §1.3.2 / §2.2)。
//
// 它是普通结构体,由组合根构造并注入;**没有包级全局变量**
// (AGENTS §10 红线)。它可安全并发使用。
type Engine struct {
limits Limits
reg *Registry
// sem 是全局并发信号量。容量 = Limits.Concurrency;槽满即 ErrBusy
// (快速失败、不入队)。
sem chan struct{}
log *slog.Logger
// codecs 是选项里攒下的待注册列表,在 NewEngine 末尾统一注册(见 WithCodecs)。
codecs []Codec
// regErr 记录注册期的第一个错误(不该发生:DefaultCodecs 是已知良好的)。
// 它不阻塞构造,但绝不静默——组合根可读 RegistrationError 并告警。
regErr error
}
// Compile-time assertion that *Engine satisfies the Transcoder interface.
var _ Transcoder = (*Engine)(nil)
// Option 配置 Engine。
type Option func(*Engine)
// WithCodecs 把 codecs 交给引擎(在 NewEngine 的所有选项都应用完之后统一注册,
// 这样即使调用方按 design/25 §6.1 的顺序先写 WithCodecs 再写 WithLogger,
// 注册失败的告警也能落到正确的日志器上)。注册失败(重复 / 不合法)是程序错误:
// 记 Error 日志并留在 RegistrationError 里,绝不静默吞掉。
func WithCodecs(codecs ...Codec) Option {
return func(e *Engine) {
e.codecs = append(e.codecs, codecs...)
}
}
// WithLogger 设置引擎日志。日志一律带 source=capability/transcode
// (AGENTS §10:日志必须标来源)。
func WithLogger(l *slog.Logger) Option {
return func(e *Engine) {
e.log = loggerFor(l)
}
}
// loggerFor 把 nil 日志收敛成一个丢弃日志:本包不因为「没人给日志」而崩,
// 也不因为「没人给日志」而少一次调用记录(默认丢弃,责任在组合根)。
func loggerFor(l *slog.Logger) *slog.Logger {
if l == nil {
return slog.New(slog.DiscardHandler)
}
return l
}
// NewEngine 构造一个引擎。limits 的零值字段由 DefaultLimits 补足;
// 未给 WithCodecs 时注册表为空 ⇒ Supports 恒 false、Transcode 恒
// ErrUnsupportedFormat(design/25 §8-T1 的空注册表行为)。
func NewEngine(limits Limits, opts ...Option) *Engine {
e := &Engine{
limits: limits.withDefaults(),
reg: NewRegistry(),
sem: make(chan struct{}, limits.withDefaults().Concurrency),
log: loggerFor(nil),
}
for _, opt := range opts {
if opt != nil {
opt(e)
}
}
e.registerCodecs()
return e
}
// registerCodecs 注册 WithCodecs 攒下的编解码器(在所有选项都生效之后)。
func (e *Engine) registerCodecs() {
for _, c := range e.codecs {
if err := e.reg.Register(c); err != nil {
if e.regErr == nil {
e.regErr = err
}
e.log.Error("transcode codec registration skipped", "error", err)
}
}
e.codecs = nil
}
// RegistrationError 返回构造期第一个编解码器注册错误(正常为 nil)。
func (e *Engine) RegistrationError() error { return e.regErr }
// Limits 返回生效的限额(零值已补默认)。
func (e *Engine) Limits() Limits { return e.limits }
// Supports 报告 src→dst 这条路是否可用(能力门)。
func (e *Engine) Supports(src, dst Format) bool {
_, ok := e.reg.Lookup(src, dst)
return ok
}
// Formats 报告当前注册的所有格式(发现)。
func (e *Engine) Formats() []Format { return e.reg.Formats() }
// Transcode 把 req.Data 从 req.Source 转成 req.Target(design/25 §2.1)。
//
// 失败一律返回 errors.go 的哨兵之一(或包裹它)——ctx 截止返回 ErrTimeout,
// 调用方主动取消返回 context.Canceled 的包裹(取消是调用方自己的决定,
// 与「转码失败」不是一回事)。进入编解码器之前的每一道闸都在**解码前**:
// 未知格式、越限、槽满都不会解出一个字节。
//
// ctx 为 nil 时按 context.Background() 处理;ctx 超时是硬闸,绝不无界等待,
// 也不泄漏 goroutine(本调用全程在调用方 goroutine 里跑完)。
func (e *Engine) Transcode(ctx context.Context, req Request) (Result, error) {
start := time.Now()
if ctx == nil {
ctx = context.Background()
}
src, err := e.resolveSource(req)
if err != nil {
return Result{}, e.reject(ctx, req, src, err, start)
}
if req.Target == "" {
return Result{}, e.reject(ctx, req, src,
fmt.Errorf("%w: empty target format", ErrUnsupportedFormat), start)
}
codec, ok := e.reg.Lookup(src, req.Target)
if !ok {
return Result{}, e.reject(ctx, req, src,
fmt.Errorf("%w: no codec registered", ErrUnsupportedFormat), start)
}
if len(req.Data) > e.limits.MaxInputBytes {
return Result{}, e.reject(ctx, req, src,
fmt.Errorf("%w: %d bytes > %d", ErrInputTooLarge, len(req.Data), e.limits.MaxInputBytes), start)
}
if err := ctx.Err(); err != nil {
return Result{}, e.reject(ctx, req, src, timeoutError(err), start)
}
if !e.acquire() {
return Result{}, e.reject(ctx, req, src,
fmt.Errorf("%w: %d slots busy", ErrBusy, e.limits.Concurrency), start)
}
defer e.release()
callCtx, cancel := context.WithTimeout(ctx, e.limits.Timeout)
defer cancel()
res, err := codec.Transcode(callCtx, req.Data, e.limits)
if err != nil {
return Result{}, e.reject(ctx, req, src, classify(callCtx, err), start)
}
if len(res.Data) == 0 {
return Result{}, e.reject(ctx, req, src,
fmt.Errorf("%w: codec returned empty output", ErrEncodeFailed), start)
}
e.log.Info("transcode ok",
"source_format", FormatName(res.Source),
"target_format", FormatName(res.Format),
"in_bytes", len(req.Data),
"out_bytes", len(res.Data),
"frames", res.Frames,
"duration_ms", res.DurationMs,
"elapsed_ms", time.Since(start).Milliseconds())
return res, nil
}
// resolveSource 定出这次请求的实际来源格式:显式给出就用它,留空则按魔数嗅探,
// 嗅不出即 ErrUnsupportedFormat(deny-by-default)。
func (e *Engine) resolveSource(req Request) (Format, error) {
if req.Source != "" {
return req.Source, nil
}
sniffed, ok := Sniff(req.Data)
if !ok {
return "", fmt.Errorf("%w: magic not recognized", ErrUnsupportedFormat)
}
return sniffed, nil
}
// acquire 非阻塞地取一个并发槽。拿不到即 false——绝不排队。
func (e *Engine) acquire() bool {
select {
case e.sem <- struct{}{}:
return true
default:
return false
}
}
// release 归还一个并发槽。
func (e *Engine) release() { <-e.sem }
// reject 记一次失败日志并把错误包上**路由上下文**后返回(绝不静默、绝不裸返回):
// 最终文案形如 `transcode: amr -> wav: unsupported format: no codec registered`。
// 可预期的输入问题(哨兵)记 Warn;不该发生的(非哨兵:编解码器违约)记 Error。
func (e *Engine) reject(ctx context.Context, req Request, src Format, err error, start time.Time) error {
level := slog.LevelWarn
if !isSentinelError(err) {
level = slog.LevelError
}
e.log.Log(ctx, level, "transcode failed",
"source_format", FormatName(src),
"target_format", FormatName(req.Target),
"in_bytes", len(req.Data),
"elapsed_ms", time.Since(start).Milliseconds(),
"error", err)
return fmt.Errorf("transcode: %s: %w", routeLabel(src, req.Target), err)
}
// routeLabel 是错误/日志里的路由可读名;格式未知(嗅探失败 / 目标留空)时用
// `<unknown>` 占位,绝不留一段空白让人猜。
func routeLabel(src, dst Format) string {
from, to := FormatName(src), FormatName(dst)
if from == "" {
from = "<unknown>"
}
if to == "" {
to = "<unknown>"
}
return from + " -> " + to
}
// classify 把编解码器返回的错误收敛到 design/25 §2.4 的口径:ctx 截止即
// ErrTimeout;调用方取消保持 context.Canceled 的包裹;其余补一层来源后上抛
// (编解码器自己用哨兵,engine 不替它猜,但也不裸返回)。
func classify(callCtx context.Context, err error) error {
switch {
case errors.Is(err, context.DeadlineExceeded), errors.Is(callCtx.Err(), context.DeadlineExceeded):
return timeoutError(context.DeadlineExceeded)
case errors.Is(err, context.Canceled), errors.Is(callCtx.Err(), context.Canceled):
return fmt.Errorf("canceled: %w", context.Canceled)
default:
return fmt.Errorf("codec failed: %w", err)
}
}
// timeoutError 把 ctx 截止统一成 ErrTimeout 的包裹,并保留原始原因。
func timeoutError(cause error) error {
return fmt.Errorf("%w: %w", ErrTimeout, cause)
}
// isSentinelError 报告 err 是否属于本包已登记的哨兵(含包内构造期错误)。
// 逐条列举而不是查一个包级切片:本包因此**没有任何包级状态**
// (AGENTS §10:禁包级全局变量,除 var ErrXxx)。
func isSentinelError(err error) bool {
return errors.Is(err, ErrUnsupportedFormat) ||
errors.Is(err, ErrInputTooLarge) ||
errors.Is(err, ErrOutputTooLarge) ||
errors.Is(err, ErrDecodeFailed) ||
errors.Is(err, ErrEncodeFailed) ||
errors.Is(err, ErrTimeout) ||
errors.Is(err, ErrBusy) ||
errors.Is(err, ErrNoBackend) ||
errors.Is(err, ErrDuplicate) ||
errors.Is(err, ErrInvalidCodec) ||
errors.Is(err, ErrBadArgs)
}