Files
Cyrene_v2/internal/capability/transcode/tool.go
AskaEth 4a2d276881
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
2026-10-05 13:02:28 +08:00

130 lines
5.0 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 transcode
import (
"context"
"encoding/base64"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// 本文件是 design/25 §1.3.2 的**第二层注册**:把转码暴露成 agent 可选用的内置
// 工具(KindTool),走**既有**的 sdk.Registrar → internal/capability/registry。
// 与 vision.RegisterTools / io.Register 逐字同族,因此面板、策略、审计三面零新增
// 代码即覆盖;**不新增 Kind**(validKind 闭集不动),策略默认零值 = owner-only。
//
// ⚠️ 本工具是 design/25 §8-T1 的「RegisterTool 壳」:T4 定义的形态是
// `{name, to}`(服务端存储名进、新存储名出,薄壳:读存储 → 调 core → 写存储),
// 它依赖 design/24 §10 的音频副本存储与 `GET /api/v1/media/{name}`——那两件**尚未
// 落地**。因此本壳用「字节进、字节出」(base64 承载)先把**注册链路**打通并可测,
// T4 落地后再换入参形态;这条差异已登记在交接报告里。
//
// 红线提醒:转码产物是**字节**,不进上下文。因此本壳工具另设一个远小于
// MaxOutputBytes 的结果上限(MaxToolResultBytes),让「工具结果」不可能把上下文
// 灌爆——要转大文件请走进程内 DI 路径,不要走模型工具面。
const (
// ToolName 是转码工具在全局能力注册表里的名字。
ToolName = "transcode"
// MaxToolResultBytes 是本壳工具允许回给模型的产物体积上限(解码后字节数)。
// 它**不是** design/25 §2.5 的 MaxOutputBytes(那是能力上限,64 MiB);这里更小
// 是因为工具结果会进模型上下文。
MaxToolResultBytes = 256 << 10
)
// 入参字段名。抽成常量以免同一个键在三处各写一份字符串。
const (
toolArgData = "data_base64"
toolArgSource = "source"
toolArgTarget = "target"
)
// Tool 把 Transcoder 包装成 sdk.Tool,使 agent 能直接调用转码(design/25 §9-2
// 默认「暴露,owner-only」)。它可安全并发使用。
type Tool struct {
tr Transcoder
}
// Compile-time assertion that *Tool satisfies the SDK Tool interface.
var _ sdk.Tool = (*Tool)(nil)
// NewTool 用给定转码器构造工具。
func NewTool(tr Transcoder) *Tool { return &Tool{tr: tr} }
// Name 返回工具标识。
func (t *Tool) Name() string { return ToolName }
// Description 说明该工具做什么。它让模型知道「转码是本地能力、同步、不看身份」。
func (t *Tool) Description() string {
return "Convert audio bytes between formats locally (e.g. amr -> wav). " +
"Input and output are base64-encoded bytes; the result carries format, " +
"sample rate, channels, duration and frame count."
}
// Parameters 返回输入 JSON Schema:data_base64(必填)+ target(必填)+ source(可选)。
func (t *Tool) Parameters() (map[string]any, error) {
return map[string]any{
"type": "object",
"properties": map[string]any{
toolArgData: map[string]any{
"type": "string",
"description": "The source audio bytes, base64-encoded (standard alphabet).",
},
toolArgTarget: map[string]any{
"type": "string",
"description": "Target format name, lowercase and without a dot, e.g. \"wav\".",
},
toolArgSource: map[string]any{
"type": "string",
"description": "Optional source format name; omitted means sniff by magic bytes.",
},
},
"required": []any{toolArgData, toolArgTarget},
}, nil
}
// Execute 解码入参、调用转码器,并把结果包成模型可读的对象。
// 失败一律返回 errors.go 的哨兵(经 %w 保留),绝不假装成功。
func (t *Tool) Execute(ctx context.Context, args any) (any, error) {
if t.tr == nil {
return nil, fmt.Errorf("transcode: tool %q: %w: nil transcoder", ToolName, ErrUnsupportedFormat)
}
req, err := toolRequest(args)
if err != nil {
return nil, err
}
res, err := t.tr.Transcode(ctx, req)
if err != nil {
return nil, fmt.Errorf("transcode: tool %q: %w", ToolName, err)
}
if len(res.Data) > MaxToolResultBytes {
return nil, fmt.Errorf("transcode: tool %q: %w: result %d bytes > %d (tool face is bounded; use the in-process API for larger audio)",
ToolName, ErrOutputTooLarge, len(res.Data), MaxToolResultBytes)
}
return map[string]any{
"format": FormatName(res.Format),
"source": FormatName(res.Source),
"data_base64": base64.StdEncoding.EncodeToString(res.Data),
"sample_rate": res.SampleRate,
"channels": res.Channels,
"duration_ms": res.DurationMs,
"frames": res.Frames,
}, nil
}
// RegisterTool 经 SDK 辅助函数把转码工具注册到 r(design/25 §1.3.2 第二层)。
// 与 vision.RegisterTools 同族:tr 为 nil 时不注册(返回 nil,不制造一个必然
// 失败的能力条目)。
func RegisterTool(r sdk.Registrar, tr Transcoder) error {
if r == nil {
return sdk.ErrNilRegistrar
}
if tr == nil {
return nil
}
tool := NewTool(tr)
if err := sdk.RegisterTool(r, tool); err != nil {
return fmt.Errorf("transcode: register tool %q: %w", tool.Name(), err)
}
return nil
}