776ef6fda9
maxVideoResponseBytes (512MB) replaces the shared 64MB JSON cap on the /v1/videos/sync read path (doRaw now takes the cap per call) — 3/6 models flagged that a legitimate long/high-bitrate clip would be discarded after minutes of GPU work. Plus a stale stable-diffusion comment in initImageFilename and a test-handler early return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXQxVhXBw8PwFAtsVrXSmj
226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
package llamaswap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/videogen"
|
|
)
|
|
|
|
func TestVideoGenerate(t *testing.T) {
|
|
var gotPath, gotContentType string
|
|
var gotForm map[string]string
|
|
var gotFrame []byte
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotContentType = r.Header.Get("Content-Type")
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
|
t.Errorf("parse form: %v", err)
|
|
return
|
|
}
|
|
gotForm = map[string]string{}
|
|
for k, v := range r.MultipartForm.Value {
|
|
gotForm[k] = v[0]
|
|
}
|
|
if f, _, err := r.FormFile("input_reference"); err == nil {
|
|
gotFrame, _ = io.ReadAll(f)
|
|
f.Close()
|
|
}
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
_, _ = w.Write([]byte("fake-mp4-bytes"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
|
|
vm, err := p.VideoModel("videogen-wan")
|
|
if err != nil {
|
|
t.Fatalf("VideoModel: %v", err)
|
|
}
|
|
|
|
frame, _ := base64.StdEncoding.DecodeString(onePixelPNG)
|
|
res, err := vm.Generate(context.Background(),
|
|
videogen.Request{Prompt: "a cat surfing", InitImage: &videogen.Image{MIME: "image/png", Data: frame}},
|
|
videogen.WithSize("1280x704"),
|
|
videogen.WithNumFrames(81),
|
|
videogen.WithFPS(16),
|
|
videogen.WithSteps(4),
|
|
videogen.WithGuidanceScale(1.0),
|
|
videogen.WithNegativePrompt("blurry"),
|
|
videogen.WithSeed(42),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if string(res.Video.Data) != "fake-mp4-bytes" || res.Video.MIME != "video/mp4" {
|
|
t.Fatalf("video = %d bytes, MIME %q", len(res.Video.Data), res.Video.MIME)
|
|
}
|
|
|
|
if gotPath != "/v1/videos/sync" {
|
|
t.Errorf("path = %q", gotPath)
|
|
}
|
|
if !strings.HasPrefix(gotContentType, "multipart/form-data") {
|
|
t.Errorf("content-type = %q", gotContentType)
|
|
}
|
|
want := map[string]string{
|
|
"model": "videogen-wan",
|
|
"prompt": "a cat surfing",
|
|
"negative_prompt": "blurry",
|
|
"width": "1280",
|
|
"height": "704",
|
|
"num_frames": "81",
|
|
"fps": "16",
|
|
"num_inference_steps": "4",
|
|
"guidance_scale": "1",
|
|
"seed": "42",
|
|
}
|
|
for k, v := range want {
|
|
if gotForm[k] != v {
|
|
t.Errorf("form[%q] = %q, want %q", k, gotForm[k], v)
|
|
}
|
|
}
|
|
if string(gotFrame) != string(frame) {
|
|
t.Errorf("input_reference = %d bytes, want %d", len(gotFrame), len(frame))
|
|
}
|
|
}
|
|
|
|
func TestVideoGenerateOmitsUnsetOverrides(t *testing.T) {
|
|
var gotForm map[string][]string
|
|
var hadFrame bool
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_ = r.ParseMultipartForm(32 << 20)
|
|
gotForm = r.MultipartForm.Value
|
|
_, _, err := r.FormFile("input_reference")
|
|
hadFrame = err == nil
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
_, _ = w.Write([]byte("v"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
|
|
vm, _ := p.VideoModel("videogen-wan")
|
|
if _, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"}); err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
for _, k := range []string{"negative_prompt", "width", "height", "num_frames", "fps", "num_inference_steps", "guidance_scale", "seed"} {
|
|
if _, ok := gotForm[k]; ok {
|
|
t.Errorf("form field %q sent, want omitted", k)
|
|
}
|
|
}
|
|
if hadFrame {
|
|
t.Error("input_reference sent, want omitted")
|
|
}
|
|
}
|
|
|
|
func TestVideoGenerateValidation(t *testing.T) {
|
|
p := New(WithBaseURL("http://unused.invalid"))
|
|
vm, _ := p.VideoModel("videogen-wan")
|
|
|
|
cases := []struct {
|
|
name string
|
|
req videogen.Request
|
|
}{
|
|
{"empty prompt", videogen.Request{}},
|
|
{"negative frames", videogen.Request{Prompt: "x", NumFrames: -1}},
|
|
{"negative fps", videogen.Request{Prompt: "x", FPS: -1}},
|
|
{"empty init image", videogen.Request{Prompt: "x", InitImage: &videogen.Image{}}},
|
|
{"bad size", videogen.Request{Prompt: "x", Size: "banana"}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := vm.Generate(context.Background(), tc.req)
|
|
if !errors.Is(err, llm.ErrUnsupported) {
|
|
t.Fatalf("err = %v, want ErrUnsupported", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVideoGenerateUpstreamError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, `{"error":{"message":"boom"}}`, http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
|
|
vm, _ := p.VideoModel("videogen-wan")
|
|
_, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"})
|
|
var apiErr *llm.APIError
|
|
if !errors.As(err, &apiErr) {
|
|
t.Fatalf("err = %v, want *llm.APIError", err)
|
|
}
|
|
}
|
|
|
|
func TestVideoGenerateEmptyResponse(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
|
|
vm, _ := p.VideoModel("videogen-wan")
|
|
_, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"})
|
|
var apiErr *llm.APIError
|
|
if !errors.As(err, &apiErr) {
|
|
t.Fatalf("err = %v, want *llm.APIError for empty body", err)
|
|
}
|
|
}
|
|
|
|
func TestVideoModelRequiresBaseURL(t *testing.T) {
|
|
p := New()
|
|
if _, err := p.VideoModel("videogen-wan"); err == nil {
|
|
t.Fatal("VideoModel with no base URL should error")
|
|
}
|
|
}
|
|
|
|
func TestVideoMIME(t *testing.T) {
|
|
// A minimal ISO-BMFF prefix http.DetectContentType sniffs as video/mp4
|
|
// (the "mp4" brand prefix must appear inside the declared ftyp box).
|
|
mp4Magic := append([]byte{0, 0, 0, 20}, []byte("ftypmp42\x00\x00\x00\x00mp42")...)
|
|
cases := []struct {
|
|
contentType string
|
|
data []byte
|
|
want string
|
|
}{
|
|
{"video/webm", []byte("x"), "video/webm"},
|
|
{"video/mp4; charset=binary", []byte("x"), "video/mp4"},
|
|
{"application/octet-stream", mp4Magic, "video/mp4"},
|
|
// Neither declared nor sniffable as video → "" (Generate errors).
|
|
{"application/octet-stream", []byte(`{"id":"job-1"}`), ""},
|
|
{"", []byte("x"), ""},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := videoMIME(tc.contentType, tc.data); got != tc.want {
|
|
t.Errorf("videoMIME(%q, %q) = %q, want %q", tc.contentType, tc.data, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVideoGenerateNonVideoBodyErrors(t *testing.T) {
|
|
// A stock async /v1/videos handler mounted at the sync path (or an HTML
|
|
// error page behind a proxy) answers 200 with a non-video body — that
|
|
// must be an error, never a "successful" garbage clip.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"job-1","status":"queued"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
|
|
vm, _ := p.VideoModel("videogen-wan")
|
|
_, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"})
|
|
var apiErr *llm.APIError
|
|
if !errors.As(err, &apiErr) {
|
|
t.Fatalf("err = %v, want *llm.APIError for non-video 2xx body", err)
|
|
}
|
|
if !strings.Contains(apiErr.Message, "not a video") {
|
|
t.Errorf("message = %q, want mention of non-video body", apiErr.Message)
|
|
}
|
|
}
|