158 lines
6.4 KiB
Go
158 lines
6.4 KiB
Go
package protocol
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDoubaoStreamingTTSPackageBuildsOfficialRequest(t *testing.T) {
|
|
manifest := []byte(`{
|
|
"apiVersion":"yingce.plugin/v2",
|
|
"id":"doubao-streaming-tts","name":"Doubao Streaming TTS","version":"2.0.0","author":"Volcengine",
|
|
"contributes":{"providers":[{"id":"doubao-streaming-tts","label":"Doubao Streaming TTS","capabilities":["audio"],"scopes":["canvas"],"auth":{"type":"header","field":"apiKey","header":"X-Api-Key"},"create":{"method":"POST","path":"/api/v3/tts/unidirectional","contentType":"application/json","body":{"req_params":{"$json":{"text":{"$ref":"request.prompt"},"speaker":{"$ref":"request.extra.audioVoice"},"model":{"$ref":"request.model"}}}},"headers":{"X-Api-Resource-Id":{"$coalesce":[{"$ref":"request.providerOptions.doubao-streaming-tts.resourceId"},"seed-tts-2.0"]},"X-Api-Request-Id":{"$ref":"request.extra.idempotencyKey"}}},"response":{"streamedJsonAudio":true,"resultKind":"audio"}}]}
|
|
}`)
|
|
adapter, err := LoadManifest(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
|
Capability: CapabilityAudio,
|
|
Model: "seed-tts-2.0",
|
|
Prompt: "你好",
|
|
Extra: map[string]any{"audioVoice": "speaker-1", "idempotencyKey": "request-1"},
|
|
ProviderOptions: map[string]map[string]any{
|
|
"doubao-streaming-tts": {"resourceId": "seed-icl-2.0"},
|
|
},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if spec.Headers["X-Api-Resource-Id"] != "seed-icl-2.0" || spec.Headers["X-Api-Request-Id"] != "request-1" {
|
|
t.Fatalf("headers = %#v", spec.Headers)
|
|
}
|
|
body, ok := spec.Body.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("body type = %T", spec.Body)
|
|
}
|
|
params, ok := body["req_params"].(string)
|
|
if !ok {
|
|
t.Fatalf("req_params type = %T, want JSON string", body["req_params"])
|
|
}
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal([]byte(params), &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decoded["text"] != "你好" || decoded["speaker"] != "speaker-1" || decoded["model"] != "seed-tts-2.0" {
|
|
t.Fatalf("req_params = %#v", decoded)
|
|
}
|
|
}
|
|
|
|
func TestStreamedJSONAudioCreateResultConcatenatesFrames(t *testing.T) {
|
|
first := base64.StdEncoding.EncodeToString([]byte("first"))
|
|
second := base64.StdEncoding.EncodeToString([]byte("second"))
|
|
body := []byte(`{"code":0,"data":"` + first + `"}{"code":0,"data":"` + second + `","usage":{"characters":6}}`)
|
|
adapter := manifestAdapter{manifest: Manifest{Response: ManifestResponse{StreamedJSONAudio: true, ResultKind: "audio"}}}
|
|
result, err := adapter.ParseCreate(context.Background(), body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Status != StatusSucceeded || result.Result == nil || len(result.Result.Audios) != 1 {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
if !strings.HasPrefix(result.Result.Audios[0].DataURL, "data:audio/mpeg;base64,") {
|
|
t.Fatalf("data URL = %q", result.Result.Audios[0].DataURL)
|
|
}
|
|
encoded := strings.TrimPrefix(result.Result.Audios[0].DataURL, "data:audio/mpeg;base64,")
|
|
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
|
if err != nil || string(decoded) != "firstsecond" {
|
|
t.Fatalf("audio bytes = %q, err = %v", decoded, err)
|
|
}
|
|
}
|
|
|
|
func TestStreamedJSONAudioCreateResultRejectsBusinessError(t *testing.T) {
|
|
adapter := manifestAdapter{manifest: Manifest{Response: ManifestResponse{StreamedJSONAudio: true, ResultKind: "audio"}}}
|
|
_, err := adapter.ParseCreate(context.Background(), []byte(`{"code":30000001,"message":"invalid speaker"}`))
|
|
if err == nil || !strings.Contains(err.Error(), "invalid speaker") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDoubaoSeedAudioCreateUsesTextPromptWithoutResourceHeader(t *testing.T) {
|
|
manifest, err := os.ReadFile("../../../plugin-packages/doubao-streaming-tts/manifest.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adapter, err := LoadManifest(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{Capability: CapabilityAudio, Model: "seed-audio-1.0", Prompt: "你好"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if spec.Path != "/api/v3/tts/create" {
|
|
t.Fatalf("path = %s", spec.Path)
|
|
}
|
|
if _, ok := spec.Headers["X-Api-Resource-Id"]; ok {
|
|
t.Fatalf("seed-audio create must not send a TTS resource header: %#v", spec.Headers)
|
|
}
|
|
body := spec.Body.(map[string]any)
|
|
if body["model"] != "seed-audio-1.0" || body["text_prompt"] != "你好" || body["references"] != nil {
|
|
t.Fatalf("text-only body = %#v", body)
|
|
}
|
|
}
|
|
|
|
func TestDoubaoSeedAudioCreateSendsReferenceAudioAndImage(t *testing.T) {
|
|
manifest, err := os.ReadFile("../../../plugin-packages/doubao-streaming-tts/manifest.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adapter, err := LoadManifest(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
audioSpec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
|
Capability: CapabilityAudio,
|
|
Model: "seed-audio-1.0",
|
|
Prompt: "@Audio1 用这个声音说你好",
|
|
Audios: []MediaReference{{URL: "https://cdn.example.com/voice.mp3", Kind: "audio"}},
|
|
Extra: map[string]any{"audioVoice": "zh_female_qingxinnvsheng_uranus_bigtts", "audioFormat": "mp3"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
audioBody := audioSpec.Body.(map[string]any)
|
|
references, _ := audioBody["references"].([]any)
|
|
if len(references) != 2 {
|
|
t.Fatalf("audio references = %#v", audioBody["references"])
|
|
}
|
|
reference, _ := references[0].(map[string]any)
|
|
speaker, _ := references[1].(map[string]any)
|
|
if reference["audio_url"] != "https://cdn.example.com/voice.mp3" || reference["audio_data"] != nil || speaker["speaker"] != "zh_female_qingxinnvsheng_uranus_bigtts" {
|
|
t.Fatalf("audio references = %#v", references)
|
|
}
|
|
|
|
imageSpec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
|
Capability: CapabilityAudio,
|
|
Model: "seed-audio-1.0",
|
|
Prompt: "根据图片内容生成音频",
|
|
Images: []MediaReference{{DataURL: "data:image/png;base64,aGVsbG8=", Kind: "image"}},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
imageBody := imageSpec.Body.(map[string]any)
|
|
imageReferences, _ := imageBody["references"].([]any)
|
|
if len(imageReferences) != 1 {
|
|
t.Fatalf("image references = %#v", imageBody["references"])
|
|
}
|
|
imageReference, _ := imageReferences[0].(map[string]any)
|
|
if imageReference["image_data"] != "aGVsbG8=" || imageReference["image_url"] != nil {
|
|
t.Fatalf("image reference = %#v", imageReference)
|
|
}
|
|
}
|