feat!: 账号池改渠道网关,对外协议换成 vivid /v1
- 新增 channels/channel_models:上游 URL + AES-GCM 加密 key +
并发/权重/本地统计,模型经渠道 /v1/models 导入绑定
- internal/upstream:vivid 协议客户端(NDJSON 流 + jobId 断流重连 +
uploads 预签名 + models + balance + 错误分类)
- service/v1 重写:权重分组调度、Redis 并发槽 conc:c:、故障转移,
站内/测试/对外统一走渠道执行器
- 对外端点换成 vivid 四件套:POST /v1/generate(NDJSON+jobId 重连)、
POST /v1/uploads(SigV4 预签名 PUT)、GET /v1/models、GET /v1/balance,
错误包 {"error":{"code","message"}}
- API key 新发改 vk_+48hex;删除全部内置 provider 与 token 池
- 管理端:渠道 CRUD/拉模型/导入/余额端点,前端账号页换渠道页,
概览/文档/测试弹窗适配
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
)
|
||||
|
||||
// maxMediaDurationSec 时长 sanity 上限:解析出超过 24h 视为垃圾数据丢弃。
|
||||
const maxMediaDurationSec = 24 * 3600
|
||||
|
||||
// MediaDurationSeconds 从已读片段尽力解析媒体时长(秒)。
|
||||
// mp4/mov/m4a 扫描 mvhd 盒子;webm 解 EBML Duration×TimecodeScale;wav 按
|
||||
// byteRate 换算。解析不出返回 false——调用方可用文件尾片段再试一次
|
||||
// (未 faststart 的 mp4 moov 盒子常在文件尾)。mp3/ogg 无廉价可靠头长度,跳过。
|
||||
func MediaDurationSeconds(mime string, buf []byte) (float64, bool) {
|
||||
if len(buf) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
var sec float64
|
||||
var ok bool
|
||||
switch mime {
|
||||
case "video/mp4", "video/quicktime", "audio/mp4":
|
||||
sec, ok = mvhdDuration(buf)
|
||||
case "video/webm":
|
||||
sec, ok = webmDuration(buf)
|
||||
case "audio/wav":
|
||||
sec, ok = wavDuration(buf)
|
||||
}
|
||||
if !ok || sec <= 0 || sec > maxMediaDurationSec {
|
||||
return 0, false
|
||||
}
|
||||
return sec, true
|
||||
}
|
||||
|
||||
// mvhdDuration 在缓冲里扫 "mvhd" 盒子并解析 duration/timescale。
|
||||
// 盒子布局:[size4][type4=mvhd][version1][flags3]
|
||||
// v0: creation4 mod4 timescale4 duration4 → timescale@idx+16, duration@idx+20
|
||||
// v1: creation8 mod8 timescale4 duration8 → timescale@idx+24, duration@idx+28
|
||||
func mvhdDuration(buf []byte) (float64, bool) {
|
||||
for off := 0; ; {
|
||||
i := bytes.Index(buf[off:], []byte("mvhd"))
|
||||
if i < 0 {
|
||||
return 0, false
|
||||
}
|
||||
i += off
|
||||
if ver := buf[i+4]; ver == 0 && i+24 <= len(buf) {
|
||||
ts := binary.BigEndian.Uint32(buf[i+16 : i+20])
|
||||
dur := binary.BigEndian.Uint32(buf[i+20 : i+24])
|
||||
if ts > 0 && dur > 0 {
|
||||
return float64(dur) / float64(ts), true
|
||||
}
|
||||
} else if ver == 1 && i+36 <= len(buf) {
|
||||
ts := binary.BigEndian.Uint32(buf[i+24 : i+28])
|
||||
dur := binary.BigEndian.Uint64(buf[i+28 : i+36])
|
||||
if ts > 0 && dur > 0 {
|
||||
return float64(dur) / float64(ts), true
|
||||
}
|
||||
}
|
||||
off = i + 4
|
||||
}
|
||||
}
|
||||
|
||||
// webmDuration 扫 EBML Duration 元素(0x4489,float)和 TimecodeScale
|
||||
// (0x2AD7B1,uint,缺省 1,000,000ns)。秒 = duration × scale / 1e9。
|
||||
func webmDuration(buf []byte) (float64, bool) {
|
||||
scale := uint64(1_000_000)
|
||||
if i := bytes.Index(buf, []byte{0x2a, 0xd7, 0xb1}); i >= 0 {
|
||||
if v, n := ebmlVint(buf[i+3:]); n > 0 && i+3+n+v <= len(buf) {
|
||||
scale = 0
|
||||
for _, b := range buf[i+3+n : i+3+n+v] {
|
||||
scale = scale<<8 | uint64(b)
|
||||
}
|
||||
if scale == 0 {
|
||||
scale = 1_000_000
|
||||
}
|
||||
}
|
||||
}
|
||||
i := bytes.Index(buf, []byte{0x44, 0x89})
|
||||
if i < 0 {
|
||||
return 0, false
|
||||
}
|
||||
v, n := ebmlVint(buf[i+2:])
|
||||
if n == 0 || i+2+n+v > len(buf) {
|
||||
return 0, false
|
||||
}
|
||||
f := buf[i+2+n : i+2+n+v]
|
||||
var dur float64
|
||||
switch v {
|
||||
case 4:
|
||||
dur = float64(math.Float32frombits(binary.BigEndian.Uint32(f)))
|
||||
case 8:
|
||||
dur = math.Float64frombits(binary.BigEndian.Uint64(f))
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
return dur * float64(scale) / 1e9, true
|
||||
}
|
||||
|
||||
// ebmlVint 读 EBML 变长整数的长度值:返回 (负载长度, vint 本身字节数)。
|
||||
func ebmlVint(buf []byte) (int, int) {
|
||||
if len(buf) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
for n := 1; n <= 8; n++ {
|
||||
mask := byte(0x80 >> (n - 1))
|
||||
if buf[0]&mask != 0 {
|
||||
if len(buf) < n {
|
||||
return 0, 0
|
||||
}
|
||||
v := int(buf[0] &^ mask)
|
||||
for i := 1; i < n; i++ {
|
||||
v = v<<8 | int(buf[i])
|
||||
}
|
||||
return v, n
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// wavDuration 解析 RIFF/WAVE:byteRate@28(LE u32) + "data" 块大小换算秒数。
|
||||
func wavDuration(buf []byte) (float64, bool) {
|
||||
if len(buf) < 44 || string(buf[:4]) != "RIFF" || string(buf[8:12]) != "WAVE" {
|
||||
return 0, false
|
||||
}
|
||||
rate := binary.LittleEndian.Uint32(buf[28:32])
|
||||
if rate == 0 {
|
||||
return 0, false
|
||||
}
|
||||
for off := 12; off+8 <= len(buf); {
|
||||
size := int(binary.LittleEndian.Uint32(buf[off+4 : off+8]))
|
||||
if string(buf[off:off+4]) == "data" {
|
||||
return float64(size) / float64(rate), true
|
||||
}
|
||||
off += 8 + size + size&1 // 块按 2 字节对齐
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user