feat(upstream): custom 模型按媒体分类配置参考文件上限
- ModelConfig 新增 max_reference_videos / max_reference_audios (配合既有 max_reference_images);admin 读写接口透传 - custom 模型表单去掉「参考模式」下拉,改为参考图/参考视频/ 参考音频三个数量输入;合计张数走 totalRefLimit 校验 - vivid 上游 /v1/uploads 按文件魔数自动选 kind(image/video/ audio)与 mime;openai 协议上游遇视频/音频参考自动跳过换号 - videoCaps 让 custom 模型的分类上限在画图台/文档徽标生效 - reference_mode 请求级 override 放行 "media" Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -178,14 +178,13 @@ func (c *Client) vividPayload(ctx context.Context, baseURL, apiKey, model, promp
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// vividUploadRef runs the two-step upload: POST /v1/uploads {kind:"image",mime}
|
||||
// → PUT the bytes to the presigned url → returns the key for refs[].
|
||||
// vividUploadRef runs the two-step upload: POST /v1/uploads {kind,mime} → PUT
|
||||
// the bytes to the presigned url → returns the key for refs[]. kind is image /
|
||||
// video / audio, derived from the file magic (same classes the upstream
|
||||
// accepts: png/jpeg/webp · mp4/webm/mov · mp3/wav/m4a/ogg).
|
||||
func (c *Client) vividUploadRef(ctx context.Context, baseURL, apiKey string, data []byte) (string, error) {
|
||||
mime := http.DetectContentType(data)
|
||||
if mime == "" || mime == "application/octet-stream" {
|
||||
mime = "image/png"
|
||||
}
|
||||
raw, _ := json.Marshal(map[string]any{"kind": "image", "mime": mime})
|
||||
kind, mime := vividRefKindMime(data)
|
||||
raw, _ := json.Marshal(map[string]any{"kind": kind, "mime": mime})
|
||||
up, err := c.doJSON(ctx, http.MethodPost, baseURL+"/v1/uploads", apiKey, raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -213,6 +212,58 @@ func (c *Client) vividUploadRef(ctx context.Context, baseURL, apiKey string, dat
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// vividRefKindMime classifies a reference blob into the vivid "kind" (image /
|
||||
// video / audio) plus a concrete mime. Magic bytes first (DetectContentType
|
||||
// misses mov/m4a/ogg-audio), then sniffing, then a png default.
|
||||
func vividRefKindMime(data []byte) (kind, mime string) {
|
||||
n := len(data)
|
||||
// ISOBMFF box: ftyp — mp4/mov/m4a share the container; the brand decides.
|
||||
if n >= 12 && string(data[4:8]) == "ftyp" {
|
||||
brand := string(data[8:12])
|
||||
switch {
|
||||
case strings.Contains(brand, "M4A"), strings.Contains(brand, "M4B"):
|
||||
return "audio", "audio/mp4"
|
||||
case strings.Contains(brand, "qt"):
|
||||
return "video", "video/quicktime"
|
||||
default:
|
||||
return "video", "video/mp4"
|
||||
}
|
||||
}
|
||||
// WebM/MKV EBML
|
||||
if n >= 4 && data[0] == 0x1A && data[1] == 0x45 && data[2] == 0xDF && data[3] == 0xA3 {
|
||||
return "video", "video/webm"
|
||||
}
|
||||
// MP3: ID3 tag or MPEG sync
|
||||
if n >= 3 && string(data[:3]) == "ID3" {
|
||||
return "audio", "audio/mpeg"
|
||||
}
|
||||
if n >= 2 && data[0] == 0xFF && (data[1]&0xE0) == 0xE0 {
|
||||
return "audio", "audio/mpeg"
|
||||
}
|
||||
// WAV / OGG / FLAC
|
||||
if n >= 4 && string(data[:4]) == "RIFF" {
|
||||
return "audio", "audio/wav"
|
||||
}
|
||||
if n >= 4 && string(data[:4]) == "OggS" {
|
||||
return "audio", "audio/ogg"
|
||||
}
|
||||
if n >= 4 && string(data[:4]) == "fLaC" {
|
||||
return "audio", "audio/x-flac"
|
||||
}
|
||||
// Sniffed type (images land here; also catches anything odd above missed).
|
||||
if m := http.DetectContentType(data); m != "" && m != "application/octet-stream" {
|
||||
switch {
|
||||
case strings.HasPrefix(m, "video/"):
|
||||
return "video", m
|
||||
case strings.HasPrefix(m, "audio/"), m == "application/ogg":
|
||||
return "audio", m
|
||||
case strings.HasPrefix(m, "image/"):
|
||||
return "image", m
|
||||
}
|
||||
}
|
||||
return "image", "image/png"
|
||||
}
|
||||
|
||||
// vividGenerate POSTs /v1/generate and reads the NDJSON stream to the terminal
|
||||
// frame. If the stream drops before terminal, it reconnects once with the
|
||||
// jobId (same-request resume — no new task, no double charge).
|
||||
|
||||
@@ -141,6 +141,56 @@ func TestVividErrorCodes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVividRefKindMime(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
kind string
|
||||
}{
|
||||
{"png", []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, 4}, "image"},
|
||||
{"jpeg", []byte{0xFF, 0xD8, 0xFF, 0xE0, 1, 2, 3, 4, 5, 6}, "image"},
|
||||
{"mp4", []byte{0, 0, 0, 0, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, "video"},
|
||||
{"mov", []byte{0, 0, 0, 0, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, "video"},
|
||||
{"webm", []byte{0x1A, 0x45, 0xDF, 0xA3, 1, 2, 3, 4}, "video"},
|
||||
{"mp3-id3", []byte{'I', 'D', '3', 4, 0, 0, 1, 2}, "audio"},
|
||||
{"mp3-sync", []byte{0xFF, 0xFB, 1, 2, 3, 4}, "audio"},
|
||||
{"wav", []byte{'R', 'I', 'F', 'F', 1, 2, 3, 4, 'W', 'A', 'V', 'E'}, "audio"},
|
||||
{"ogg", []byte{'O', 'g', 'g', 'S', 1, 2, 3, 4}, "audio"},
|
||||
{"m4a", []byte{0, 0, 0, 0, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, "audio"},
|
||||
{"empty", []byte{}, "image"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
kind, mime := vividRefKindMime(tc.data)
|
||||
if kind != tc.kind {
|
||||
t.Errorf("%s: kind=%s mime=%s", tc.name, kind, mime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVividUploadRefKindVideo(t *testing.T) {
|
||||
var gotKind, gotMime string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v1/uploads" {
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
gotKind, _ = req["kind"].(string)
|
||||
gotMime, _ = req["mime"].(string)
|
||||
fmt.Fprintf(w, `{"key":"k1","url":"http://%s/up"}`, r.Host)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
webm := []byte{0x1A, 0x45, 0xDF, 0xA3, 1, 2, 3, 4, 5, 6, 7, 8}
|
||||
key, err := NewClient().vividUploadRef(context.Background(), srv.URL, "vk", webm)
|
||||
if err != nil {
|
||||
t.Fatalf("upload: %v", err)
|
||||
}
|
||||
if key != "k1" || gotKind != "video" || gotMime != "video/webm" {
|
||||
t.Fatalf("key=%q kind=%q mime=%q", key, gotKind, gotMime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVividUploadRefFlow(t *testing.T) {
|
||||
var putOK bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user