diff --git a/core/backend/model_identity_modalities_test.go b/core/backend/model_identity_modalities_test.go index cc77a87b8..39d7930e2 100644 --- a/core/backend/model_identity_modalities_test.go +++ b/core/backend/model_identity_modalities_test.go @@ -49,6 +49,7 @@ type recordingBackend struct { // `ModelIdentity`; the spec below asserts they are populated independently. soundGenModelField string soundGenParams map[string]string + soundGenOutput []byte } func newRecordingBackend() *recordingBackend { @@ -73,6 +74,11 @@ func (r *recordingBackend) SoundGeneration(_ context.Context, in *pb.SoundGenera r.record("SoundGeneration", in) r.soundGenModelField = in.GetModel() r.soundGenParams = in.GetParams() + if r.soundGenOutput != nil { + if err := os.WriteFile(in.GetDst(), r.soundGenOutput, 0o600); err != nil { + return nil, err + } + } return &pb.Result{Success: true}, nil } diff --git a/core/backend/soundgeneration.go b/core/backend/soundgeneration.go index 98fb64201..31ea0e7f2 100644 --- a/core/backend/soundgeneration.go +++ b/core/backend/soundgeneration.go @@ -7,6 +7,7 @@ import ( "path/filepath" "time" + "github.com/google/uuid" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/trace" "github.com/mudler/LocalAI/pkg/grpc/proto" @@ -56,7 +57,9 @@ func SoundGeneration( return "", nil, fmt.Errorf("failed creating audio directory: %s", err) } - fileName := utils.GenerateUniqueFileName(audioDir, "sound_generation", ".wav") + // Format normalization can rename .wav to .flac, so existence of the + // .wav path cannot determine whether an earlier recording owns this stem. + fileName := utils.GenerateUniqueFileName(audioDir, "sound_generation_"+uuid.NewString(), ".wav") filePath := filepath.Join(audioDir, fileName) if filePath, err = filepath.Abs(filePath); err != nil { return "", nil, fmt.Errorf("failed resolving sound generation path: %w", err) diff --git a/core/backend/soundgeneration_history_test.go b/core/backend/soundgeneration_history_test.go new file mode 100644 index 000000000..3888de03e --- /dev/null +++ b/core/backend/soundgeneration_history_test.go @@ -0,0 +1,50 @@ +package backend_test + +import ( + "context" + "os" + "path/filepath" + + "github.com/mudler/LocalAI/core/backend" + "github.com/mudler/LocalAI/core/config" + "github.com/mudler/LocalAI/pkg/audio" + "github.com/mudler/LocalAI/pkg/system" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("sound generation history", func() { + It("preserves previous recordings after FLAC extension normalization", func() { + rec := newRecordingBackend() + loader := newRecordingLoader(rec) + appCfg := config.NewApplicationConfig(config.WithSystemState(&system.SystemState{})) + appCfg.GeneratedContentDir = GinkgoT().TempDir() + cfg := identityModelCfg() + + generate := func(content []byte) string { + rec.soundGenOutput = content + path, _, err := backend.SoundGeneration(context.Background(), "music", nil, nil, nil, nil, nil, nil, "", "", nil, "", "", "", nil, nil, loader, appCfg, cfg) + Expect(err).ToNot(HaveOccurred()) + // The endpoint detects the backend's format and renames its .wav + // destination. Only the FLAC signature is needed for detection. + finalPath, contentType := audio.NormalizeAudioFile(path) + Expect(contentType).To(Equal("audio/flac")) + Expect(filepath.Ext(finalPath)).To(Equal(".flac")) + return finalPath + } + + firstAudio := []byte("fLaC first recording fixture") + secondAudio := []byte("fLaC second recording fixture") + firstPath := generate(firstAudio) + secondPath := generate(secondAudio) + + Expect(secondPath).ToNot(Equal(firstPath)) + first, err := os.ReadFile(firstPath) + Expect(err).ToNot(HaveOccurred()) + Expect(first).To(Equal(firstAudio)) + second, err := os.ReadFile(secondPath) + Expect(err).ToNot(HaveOccurred()) + Expect(second).To(Equal(secondAudio)) + }) +}) diff --git a/docs/content/features/text-to-audio.md b/docs/content/features/text-to-audio.md index 98e2a3913..c8109ba57 100644 --- a/docs/content/features/text-to-audio.md +++ b/docs/content/features/text-to-audio.md @@ -384,6 +384,10 @@ curl http://localhost:8080/v1/sound-generation -H "Content-Type: application/jso The `/v1/sound-generation` endpoint is compatible with the [ElevenLabs sound generation API](https://elevenlabs.io/docs/api-reference/sound-generation) and can produce music, sound effects, and other audio content. It responds with a binary audio file and the appropriate `Content-Type` header (for example `audio/wav`, `audio/mpeg`, `audio/flac`, or `audio/ogg`). The request body is JSON and supports two usage modes. +Each generated sound has a unique filename. The filename remains unique when +LocalAI changes its extension to match the audio format, so later generations +do not overwrite earlier recordings in the WebUI history. + **Simple mode:** | Parameter | Type | Required | Description |