init:初始化项目
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSubmitImageToVideoTaskUsesMultipartInputReference(t *testing.T) {
|
||||
var (
|
||||
gotModel string
|
||||
gotPrompt string
|
||||
gotSeconds string
|
||||
gotSize string
|
||||
gotBytes int
|
||||
)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/videos" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err := r.ParseMultipartForm(4 << 20); err != nil {
|
||||
t.Fatalf("parse multipart form: %v", err)
|
||||
}
|
||||
gotModel = r.FormValue("model")
|
||||
gotPrompt = r.FormValue("prompt")
|
||||
gotSeconds = r.FormValue("seconds")
|
||||
gotSize = r.FormValue("size")
|
||||
file, _, err := r.FormFile("input_reference")
|
||||
if err != nil {
|
||||
t.Fatalf("missing input_reference file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
t.Fatalf("read uploaded file: %v", err)
|
||||
}
|
||||
gotBytes = len(content)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"id":"task_submit_123","status":"queued"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
||||
taskID, err := client.submitImageToVideoTask(context.Background(), "sk-test", "sora-2", map[string]any{
|
||||
"prompt": "animate the scene gently",
|
||||
"duration_s": 5,
|
||||
"aspect_ratio": "16:9",
|
||||
}, imageEditReference{
|
||||
Filename: "reference.png",
|
||||
ContentType: "image/png",
|
||||
Content: tinyPNG(t),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("submitImageToVideoTask: %v", err)
|
||||
}
|
||||
|
||||
if taskID != "task_submit_123" {
|
||||
t.Fatalf("expected task id task_submit_123, got %q", taskID)
|
||||
}
|
||||
if gotModel != "sora-2" {
|
||||
t.Fatalf("expected model sora-2, got %q", gotModel)
|
||||
}
|
||||
if gotPrompt != "animate the scene gently" {
|
||||
t.Fatalf("expected prompt to round-trip, got %q", gotPrompt)
|
||||
}
|
||||
if gotSeconds != "5" {
|
||||
t.Fatalf("expected seconds 5, got %q", gotSeconds)
|
||||
}
|
||||
if gotSize != "1280x720" {
|
||||
t.Fatalf("expected 16:9 to map to 1280x720, got %q", gotSize)
|
||||
}
|
||||
if gotBytes == 0 {
|
||||
t.Fatal("expected uploaded reference bytes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchVideoTaskFallsBackToGenericTaskEnvelope(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/videos/task_fallback_123":
|
||||
http.NotFound(w, r)
|
||||
case "/v1/video/generations/task_fallback_123":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"code":"success","message":"","data":{"task_id":"task_fallback_123","status":"SUCCESS","result_url":"http://example.com/out.mp4","progress":"100%"}}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
||||
result, err := client.fetchVideoTask(context.Background(), "sk-test", "task_fallback_123")
|
||||
if err != nil {
|
||||
t.Fatalf("fetchVideoTask: %v", err)
|
||||
}
|
||||
|
||||
if result.TaskID != "task_fallback_123" {
|
||||
t.Fatalf("expected task id task_fallback_123, got %q", result.TaskID)
|
||||
}
|
||||
if result.Status != "completed" {
|
||||
t.Fatalf("expected normalized completed status, got %q", result.Status)
|
||||
}
|
||||
if result.URL != "http://example.com/out.mp4" {
|
||||
t.Fatalf("expected result url to round-trip, got %q", result.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteImageToVideoJobCompletesAndArtifactCanBeRead(t *testing.T) {
|
||||
refBytes := tinyPNG(t)
|
||||
videoBytes := []byte("not-a-real-mp4-but-good-enough-for-streaming")
|
||||
var (
|
||||
fetchCalls int32
|
||||
contentAuth string
|
||||
)
|
||||
|
||||
var localProxyURL string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/input.png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
_, _ = w.Write(refBytes)
|
||||
case "/v1/videos":
|
||||
if err := r.ParseMultipartForm(4 << 20); err != nil {
|
||||
t.Fatalf("parse multipart form: %v", err)
|
||||
}
|
||||
if got := r.FormValue("model"); got != "sora-2" {
|
||||
t.Fatalf("expected model sora-2, got %q", got)
|
||||
}
|
||||
file, _, err := r.FormFile("input_reference")
|
||||
if err != nil {
|
||||
t.Fatalf("missing input_reference: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := io.ReadAll(file); err != nil {
|
||||
t.Fatalf("read input_reference: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"id":"task_video_123","status":"queued"}`))
|
||||
case "/v1/videos/task_video_123":
|
||||
call := atomic.AddInt32(&fetchCalls, 1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if call == 1 {
|
||||
_, _ = w.Write([]byte(`{"id":"task_video_123","status":"in_progress","progress":30}`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"id":"task_video_123","status":"completed","progress":100,"metadata":{"url":"` + localProxyURL + `","format":"mp4"}}`))
|
||||
case "/v1/videos/task_video_123/content":
|
||||
contentAuth = r.Header.Get("Authorization")
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
_, _ = w.Write(videoBytes)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
localProxyURL = strings.Replace(srv.URL, "127.0.0.1", "localhost", 1) + "/v1/videos/task_video_123/content"
|
||||
|
||||
cfg := Config{
|
||||
NewAPIBaseURL: srv.URL,
|
||||
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
|
||||
SkillhubDir: makeEmptySkillhub(t),
|
||||
SessionSecret: "test-secret",
|
||||
}
|
||||
server, err := NewWithConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewWithConfig: %v", err)
|
||||
}
|
||||
|
||||
token, _, ok, err := server.store.createSession("sk-test-upstream")
|
||||
if err != nil {
|
||||
t.Fatalf("createSession: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected session creation to succeed")
|
||||
}
|
||||
current, exists, err := server.store.session(token)
|
||||
if err != nil {
|
||||
t.Fatalf("load session: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatal("expected stored session")
|
||||
}
|
||||
|
||||
record, _, err := server.store.createJob(
|
||||
"popiskill-video-image2video-basic-v1",
|
||||
"video.image2video",
|
||||
"sora-2",
|
||||
routeExecMode("video.image2video"),
|
||||
map[string]any{
|
||||
"image_url": srv.URL + "/input.png",
|
||||
"motion_prompt": "the camera slowly pushes in",
|
||||
"duration_s": 4,
|
||||
"aspect_ratio": "9:16",
|
||||
},
|
||||
"",
|
||||
"normal",
|
||||
"",
|
||||
current,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("createJob: %v", err)
|
||||
}
|
||||
|
||||
server.executeImageToVideoJob(record)
|
||||
|
||||
done, exists, err := server.store.getJob(current.User.ID, record.JobID)
|
||||
if err != nil {
|
||||
t.Fatalf("getJob: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatal("expected completed job")
|
||||
}
|
||||
if done.Status != "done" {
|
||||
t.Fatalf("expected job status done, got %q", done.Status)
|
||||
}
|
||||
if done.NewAPITaskID != "task_video_123" {
|
||||
t.Fatalf("expected persisted task id task_video_123, got %q", done.NewAPITaskID)
|
||||
}
|
||||
if len(done.ArtifactIDs) != 1 {
|
||||
t.Fatalf("expected one artifact, got %d", len(done.ArtifactIDs))
|
||||
}
|
||||
|
||||
item, ref, exists, err := server.store.artifactRef(current.User.ID, done.ArtifactIDs[0])
|
||||
if err != nil {
|
||||
t.Fatalf("artifactRef: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatal("expected artifact ref")
|
||||
}
|
||||
if item.ContentType != "video/mp4" {
|
||||
t.Fatalf("expected video/mp4 artifact, got %q", item.ContentType)
|
||||
}
|
||||
if !strings.HasSuffix(item.Filename, ".mp4") {
|
||||
t.Fatalf("expected .mp4 filename, got %q", item.Filename)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
contentType, _, reader, err := server.newapi.openResultRef(ctx, current.UpstreamKey, ref)
|
||||
if err != nil {
|
||||
t.Fatalf("openResultRef: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("read video content: %v", err)
|
||||
}
|
||||
if contentType != "video/mp4" {
|
||||
t.Fatalf("expected fetched content type video/mp4, got %q", contentType)
|
||||
}
|
||||
if string(content) != string(videoBytes) {
|
||||
t.Fatalf("unexpected streamed content: %q", string(content))
|
||||
}
|
||||
if contentAuth != "Bearer sk-test-upstream" {
|
||||
t.Fatalf("expected auth header to be forwarded to proxy content URL, got %q", contentAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveVideoSizeUsesViduResolutionLabels(t *testing.T) {
|
||||
if got := resolveVideoSize("viduq2", map[string]any{"aspect_ratio": "16:9"}, imageEditReference{}); got != "720p" {
|
||||
t.Fatalf("expected vidu 16:9 to map to 720p, got %q", got)
|
||||
}
|
||||
if got := resolveVideoSize("viduq2", map[string]any{"size": "1080p"}, imageEditReference{}); got != "1080p" {
|
||||
t.Fatalf("expected explicit vidu resolution to be preserved, got %q", got)
|
||||
}
|
||||
if got := resolveVideoSize("sora-2", map[string]any{"aspect_ratio": "16:9"}, imageEditReference{}); got != "1280x720" {
|
||||
t.Fatalf("expected sora size to remain pixel dimensions, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func tinyPNG(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
data, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH8kAAAAASUVORK5CYII=")
|
||||
if err != nil {
|
||||
t.Fatalf("decode tiny png: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func makeEmptySkillhub(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.json"), []byte(`{"version":1,"skills":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("write skillhub index: %v", err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
Reference in New Issue
Block a user