Restore MiniMax media routing parity across image and video flows

The server now keeps portable image ratio normalization, enforces documented Seedream size presets, routes MiniMax img2img through generations-style payloads, and submits Hailuo video variants through the video generations API with proper reference-image handling.

Constraint: The deployed test chain relies on popiartServer as the lowest-risk place to adapt provider-specific routing without broad newapi changes
Rejected: Expand newapi relay modes for every MiniMax image/video branch | larger blast radius than needed for the verified server-managed flows
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep MiniMax-specific media routing in popiartServer unless upstream newapi adapters are upgraded to the same contract
Tested: go test ./internal/server -run TestInferRouteKeyForModelRecognizesViduAsVideo|TestGenerate|TestResolve|TestSubmitMiniMaxVideoTask|TestExecuteImageToImageJobUsesGenerationsPathForMiniMax|TestExecuteImageToVideoJob|TestResolveVideoReferencesSupportsImagesArray
Not-tested: Full PopiNewAPI unit suite (blocked earlier by external module checksum drift in local checkout)
This commit is contained in:
wtgoku
2026-04-15 11:57:05 +08:00
parent a73b85c45f
commit 6ff9ad6997
7 changed files with 982 additions and 67 deletions
+115 -10
View File
@@ -1170,7 +1170,15 @@ func (s *Server) executeImageToImageJob(record *job) {
return
}
refs, usage, err := s.newapi.generateEditedImageRefs(ctx, record.UpstreamKey, modelID, input, ref)
var (
refs []resultRef
usage map[string]any
)
if useMiniMaxImageGenerations(modelID) {
refs, usage, err = s.newapi.generateMiniMaxImageRefs(ctx, record.UpstreamKey, modelID, input, []imageEditReference{ref})
} else {
refs, usage, err = s.newapi.generateEditedImageRefs(ctx, record.UpstreamKey, modelID, input, ref)
}
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
"model_id": modelID,
@@ -1224,18 +1232,41 @@ func (s *Server) executeImageToVideoJob(record *job) {
submitCtx, cancelSubmit := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancelSubmit()
ref, err := s.resolveImageToImageReference(submitCtx, record, input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
var refsForMiniMax []imageEditReference
var ref imageEditReference
var err error
if useMiniMaxVideoGenerations(modelID) {
if hasVideoReferenceInput(input) {
refsForMiniMax, err = s.resolveVideoReferences(submitCtx, record, input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
}
} else {
ref, err = s.resolveImageToImageReference(submitCtx, record, input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
return
}
upstreamTaskID, err := s.newapi.submitImageToVideoTask(submitCtx, record.UpstreamKey, modelID, input, ref)
var upstreamTaskID string
if useMiniMaxVideoGenerations(modelID) {
upstreamTaskID, err = s.newapi.submitMiniMaxVideoTask(submitCtx, record.UpstreamKey, modelID, input, refsForMiniMax)
} else {
upstreamTaskID, err = s.newapi.submitImageToVideoTask(submitCtx, record.UpstreamKey, modelID, input, ref)
}
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
"model_id": modelID,
@@ -1305,6 +1336,18 @@ func (s *Server) executeImageToVideoJob(record *job) {
}
}
func hasVideoReferenceInput(input map[string]any) bool {
if input == nil {
return false
}
return strings.TrimSpace(stringValue(
input["source_artifact_id"],
input["image"],
input["image_url"],
input["reference_image_url"],
)) != "" || len(extractStringValues(input["images"])) > 0
}
func (s *Server) executeUnsupportedSkill(record *job) {
if _, _, err := s.store.startJob(record.JobID); err != nil {
log.Printf("popiartServer: start job %s failed: %v", record.JobID, err)
@@ -1394,6 +1437,68 @@ func (s *Server) resolveImageToImageReference(ctx context.Context, record *job,
return s.downloadReferenceImage(ctx, refURL)
}
func (s *Server) resolveVideoReferences(ctx context.Context, record *job, input map[string]any) ([]imageEditReference, error) {
if record == nil {
return nil, fmt.Errorf("job record is required")
}
if artifactID := strings.TrimSpace(stringValue(input["source_artifact_id"])); artifactID != "" {
ref, err := s.resolveImageToImageReference(ctx, record, map[string]any{
"source_artifact_id": artifactID,
})
if err != nil {
return nil, err
}
return []imageEditReference{ref}, nil
}
urls := extractStringValues(input["images"])
if len(urls) == 0 {
if refURL := strings.TrimSpace(stringValue(
input["image"],
input["reference_image_url"],
input["image_url"],
)); refURL != "" {
urls = []string{refURL}
}
}
if len(urls) == 0 {
return nil, fmt.Errorf("reference image is required")
}
refs := make([]imageEditReference, 0, len(urls))
for _, rawURL := range urls {
ref, err := s.downloadReferenceImage(ctx, rawURL)
if err != nil {
return nil, err
}
refs = append(refs, ref)
}
return refs, nil
}
func extractStringValues(value any) []string {
switch typed := value.(type) {
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
if text := strings.TrimSpace(item); text != "" {
out = append(out, text)
}
}
return out
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
if text := strings.TrimSpace(stringValue(item)); text != "" {
out = append(out, text)
}
}
return out
default:
return nil
}
}
func (s *Server) waitForVideoTask(ctx context.Context, token, taskID string) (*videoTaskResult, error) {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()