迁移音频生成与语音合成到 gateway 并补充 simulation 测试
This commit is contained in:
@@ -2,8 +2,11 @@ package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JimengClient struct{ HTTPClient *http.Client }
|
||||
@@ -15,6 +18,7 @@ type MidjourneyClient struct{ HTTPClient *http.Client }
|
||||
type ViduClient struct{ HTTPClient *http.Client }
|
||||
type AliyunBailianClient struct{ HTTPClient *http.Client }
|
||||
type NewAPIClient struct{ HTTPClient *http.Client }
|
||||
type SunoClient struct{ HTTPClient *http.Client }
|
||||
|
||||
func (c JimengClient) Run(ctx context.Context, request Request) (Response, error) {
|
||||
return providerTaskClient{HTTPClient: c.HTTPClient, Spec: jimengSpec()}.Run(ctx, request)
|
||||
@@ -33,6 +37,9 @@ func (c HunyuanVideoClient) Run(ctx context.Context, request Request) (Response,
|
||||
}
|
||||
|
||||
func (c MinimaxClient) Run(ctx context.Context, request Request) (Response, error) {
|
||||
if request.Kind == "speech.generations" {
|
||||
return c.runSpeech(ctx, request)
|
||||
}
|
||||
return providerTaskClient{HTTPClient: c.HTTPClient, Spec: minimaxSpec()}.Run(ctx, request)
|
||||
}
|
||||
|
||||
@@ -52,6 +59,10 @@ func (c NewAPIClient) Run(ctx context.Context, request Request) (Response, error
|
||||
return providerTaskClient{HTTPClient: c.HTTPClient, Spec: newAPISpec()}.Run(ctx, request)
|
||||
}
|
||||
|
||||
func (c SunoClient) Run(ctx context.Context, request Request) (Response, error) {
|
||||
return providerTaskClient{HTTPClient: c.HTTPClient, Spec: sunoSpec()}.Run(ctx, request)
|
||||
}
|
||||
|
||||
func jimengSpec() providerTaskSpec {
|
||||
return providerTaskSpec{
|
||||
Name: "jimeng",
|
||||
@@ -149,6 +160,114 @@ func minimaxSpec() providerTaskSpec {
|
||||
}
|
||||
}
|
||||
|
||||
func (c MinimaxClient) runSpeech(ctx context.Context, request Request) (Response, error) {
|
||||
startedAt := time.Now()
|
||||
payload := minimaxSpeechPayload(request)
|
||||
result, requestID, err := providerPostJSON(ctx, httpClient(request.HTTPClient, c.HTTPClient), providerURL(request.Candidate.BaseURL, "/t2a_v2"), payload, request.Candidate.Credentials, "bearer")
|
||||
finishedAt := time.Now()
|
||||
if err != nil {
|
||||
return Response{}, annotateResponseError(err, requestID, startedAt, finishedAt)
|
||||
}
|
||||
audioHex := strings.TrimSpace(stringFromPathValue(valueAtPath(result, "data.audio")))
|
||||
if audioHex == "" {
|
||||
message := firstNonEmptyString(valueAtPath(result, "base_resp.status_msg"), valueAtPath(result, "message"), "minimax speech audio is missing")
|
||||
return Response{}, &ClientError{Code: "invalid_response", Message: message, RequestID: firstNonEmptyString(requestID, requestIDFromResult(result)), ResponseStartedAt: startedAt, ResponseFinishedAt: finishedAt, ResponseDurationMS: responseDurationMS(startedAt, finishedAt), Retryable: false}
|
||||
}
|
||||
audioBytes, err := hex.DecodeString(audioHex)
|
||||
if err != nil {
|
||||
return Response{}, &ClientError{Code: "invalid_response", Message: "minimax speech audio hex is invalid: " + err.Error(), RequestID: firstNonEmptyString(requestID, requestIDFromResult(result)), ResponseStartedAt: startedAt, ResponseFinishedAt: finishedAt, ResponseDurationMS: responseDurationMS(startedAt, finishedAt), Retryable: false}
|
||||
}
|
||||
normalized := cloneMapAny(result)
|
||||
normalized["status"] = "success"
|
||||
normalized["created"] = time.Now().UnixMilli()
|
||||
normalized["model"] = request.Model
|
||||
normalized["raw_data"] = cloneMapAny(result)
|
||||
normalized["data"] = []any{map[string]any{
|
||||
"type": "audio",
|
||||
"content": "data:audio/mpeg;base64," + base64.StdEncoding.EncodeToString(audioBytes),
|
||||
"mime_type": "audio/mpeg",
|
||||
"uploaded": false,
|
||||
}}
|
||||
return Response{
|
||||
Result: normalized,
|
||||
RequestID: firstNonEmptyString(requestID, requestIDFromResult(result)),
|
||||
Progress: providerProgress(request),
|
||||
ResponseStartedAt: startedAt,
|
||||
ResponseFinishedAt: finishedAt,
|
||||
ResponseDurationMS: responseDurationMS(startedAt, finishedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func minimaxSpeechPayload(request Request) map[string]any {
|
||||
body := cloneBody(request.Body)
|
||||
body["model"] = upstreamModelName(request.Candidate)
|
||||
voiceID := firstNonEmptyString(body["voice_id"], body["voiceId"])
|
||||
speed := firstPresent(body["speed"], float64(1))
|
||||
vol := firstPresent(body["vol"], body["volume"], float64(1))
|
||||
pitch := firstPresent(body["pitch"], float64(0))
|
||||
voiceSetting := map[string]any{
|
||||
"voice_id": voiceID,
|
||||
"speed": speed,
|
||||
"vol": vol,
|
||||
"pitch": pitch,
|
||||
}
|
||||
if emotion := firstNonEmptyString(body["emotion"]); emotion != "" {
|
||||
voiceSetting["emotion"] = emotion
|
||||
}
|
||||
delete(body, "voice_id")
|
||||
delete(body, "voiceId")
|
||||
delete(body, "speed")
|
||||
delete(body, "vol")
|
||||
delete(body, "volume")
|
||||
delete(body, "pitch")
|
||||
delete(body, "emotion")
|
||||
body["voice_setting"] = voiceSetting
|
||||
return body
|
||||
}
|
||||
|
||||
func sunoSpec() providerTaskSpec {
|
||||
return providerTaskSpec{
|
||||
Name: "suno",
|
||||
SubmitPath: func(Request, map[string]any) string { return "/generator/suno" },
|
||||
PollPath: func(_ Request, upstreamTaskID string, _ map[string]any) string {
|
||||
return "/v2/sunoinfo?id=" + upstreamTaskID
|
||||
},
|
||||
Auth: "bearer",
|
||||
TaskIDPaths: []string{"data"},
|
||||
StatusPaths: []string{"data.status"},
|
||||
SuccessStatuses: []string{"succeeded", "complete", "completed"},
|
||||
FailureStatuses: []string{"failed"},
|
||||
DefaultSubmitBody: func(request Request, body map[string]any) map[string]any {
|
||||
body["task"] = "create"
|
||||
body["model"] = sunoMappedModel(upstreamModelName(request.Candidate))
|
||||
if body["customMode"] == nil {
|
||||
body["customMode"] = false
|
||||
}
|
||||
if body["makeInstrumental"] == nil {
|
||||
body["makeInstrumental"] = false
|
||||
}
|
||||
return body
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func sunoMappedModel(model string) string {
|
||||
switch strings.TrimSpace(model) {
|
||||
case "chirp-v3-0", "chirp-v3-5":
|
||||
return "v40"
|
||||
case "chirp-v4-0":
|
||||
return "v40"
|
||||
case "chirp-v4-5":
|
||||
return "v45"
|
||||
case "chirp-v4-5+":
|
||||
return "v45+"
|
||||
case "chirp-v5-0":
|
||||
return "v50"
|
||||
default:
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
func midjourneySpec() providerTaskSpec {
|
||||
return providerTaskSpec{
|
||||
Name: "midjourney",
|
||||
|
||||
Reference in New Issue
Block a user