feat: wave-3 audio surfaces — stems, sfx, speech enhance, voice clone, translate (ADR-0024)
- audio.StemSeparator/StemSeparationProvider: Demucs zip transport via POST /upstream/<id>/v1/stems (Mode two -> two_stems=vocals; model + format fields); bounded zip unpack, entry name -> stem, ext -> MIME. - SFXModel reuses musicgen against the sync /upstream/<id>/v1/sfx route (JSON prompt/seconds/steps/cfg_scale/seed -> WAV); musicgen.Request gains CFGScale. - audio.SpeechEnhancer/SpeechEnhancementProvider: POST /upstream/<id>/v1/enhance -> WAV (result reuses SpeechResult). - SpeechRequest.ReferenceAudio/ReferenceMIME (+WithReferenceAudio): llamaswap switches to the chatterbox clone route POST /upstream/<id>/v1/audio/speech/upload (input + voice_file), wav MIME fallback. - TranscriptionRequest.Translate (+WithTranslate): translate=true form field, language=auto forced when no explicit hint (whisper.cpp default en would skip translation). - httptest contract tests (zip unpack, clone-route switch, translate + auto-language injection); ADR-0024 (index row deferred — MJ-A backfills the ADR index table and parallel edits would conflict). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -30,6 +30,17 @@ type SpeechRequest struct {
|
||||
|
||||
// Speed is the playback-rate multiplier; 0 = backend default (1.0).
|
||||
Speed float64
|
||||
|
||||
// ReferenceAudio is a short voice sample for zero-shot voice cloning
|
||||
// (chatterbox style): when set, the model speaks Input in the sampled
|
||||
// voice instead of a named Voice. nil = normal synthesis. Backends
|
||||
// without cloning support must reject a reference-carrying request
|
||||
// rather than silently ignoring it.
|
||||
ReferenceAudio []byte
|
||||
|
||||
// ReferenceMIME is the reference audio's MIME type (e.g. "audio/wav");
|
||||
// "" = let the backend sniff it.
|
||||
ReferenceMIME string
|
||||
}
|
||||
|
||||
// SpeechResult is the canonical synthesis result: raw audio bytes plus the
|
||||
@@ -59,6 +70,11 @@ func WithFormat(f string) SpeechOption { return func(r *SpeechRequest) { r.Forma
|
||||
// WithSpeed sets the playback-rate multiplier.
|
||||
func WithSpeed(s float64) SpeechOption { return func(r *SpeechRequest) { r.Speed = s } }
|
||||
|
||||
// WithReferenceAudio provides a voice sample for zero-shot voice cloning.
|
||||
func WithReferenceAudio(data []byte, mime string) SpeechOption {
|
||||
return func(r *SpeechRequest) { r.ReferenceAudio, r.ReferenceMIME = data, mime }
|
||||
}
|
||||
|
||||
// Apply returns a copy of the request with all options applied. Providers
|
||||
// call this once at the top of Speak.
|
||||
func (r SpeechRequest) Apply(opts ...SpeechOption) SpeechRequest {
|
||||
@@ -121,6 +137,13 @@ type TranscriptionRequest struct {
|
||||
|
||||
// Prompt is optional context or vocabulary to bias decoding; "" = none.
|
||||
Prompt string
|
||||
|
||||
// Translate requests an English translation of the speech instead of a
|
||||
// same-language transcript (whisper's translate task). When set and no
|
||||
// Language is given, providers must force source-language auto-detection
|
||||
// — a backend whose default language is "en" would otherwise skip
|
||||
// translation entirely.
|
||||
Translate bool
|
||||
}
|
||||
|
||||
// TranscriptionResult is the canonical transcription result.
|
||||
@@ -145,6 +168,11 @@ func WithPrompt(p string) TranscriptionOption {
|
||||
return func(r *TranscriptionRequest) { r.Prompt = p }
|
||||
}
|
||||
|
||||
// WithTranslate requests an English translation instead of a transcript.
|
||||
func WithTranslate() TranscriptionOption {
|
||||
return func(r *TranscriptionRequest) { r.Translate = true }
|
||||
}
|
||||
|
||||
// Apply returns a copy of the request with all options applied.
|
||||
func (r TranscriptionRequest) Apply(opts ...TranscriptionOption) TranscriptionRequest {
|
||||
for _, opt := range opts {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package audio
|
||||
|
||||
import "context"
|
||||
|
||||
// EnhancementRequest asks a speech-enhancement backend (DeepFilterNet style)
|
||||
// to denoise a recording. Audio is carried as bytes (never a URL), mirroring
|
||||
// TranscriptionRequest (ADR-0024).
|
||||
type EnhancementRequest struct {
|
||||
// Audio is the encoded audio to enhance.
|
||||
Audio []byte
|
||||
|
||||
// MIME is the audio MIME type (e.g. "audio/mpeg"); "" = let the backend
|
||||
// sniff it.
|
||||
MIME string
|
||||
|
||||
// Filename is the multipart filename hint some backends key their format
|
||||
// detection on; "" derives one from MIME or falls back to "audio".
|
||||
Filename string
|
||||
}
|
||||
|
||||
// EnhancementOption mutates an EnhancementRequest before it is sent.
|
||||
// Reserved for future request settings (the reference backend takes no
|
||||
// parameters).
|
||||
type EnhancementOption func(*EnhancementRequest)
|
||||
|
||||
// Apply returns a copy of the request with all options applied.
|
||||
func (r EnhancementRequest) Apply(opts ...EnhancementOption) EnhancementRequest {
|
||||
for _, opt := range opts {
|
||||
opt(&r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SpeechEnhancer denoises speech recordings. The result reuses SpeechResult
|
||||
// (audio bytes + MIME + Raw) — enhancement is audio-in/audio-out exactly
|
||||
// like synthesis.
|
||||
type SpeechEnhancer interface {
|
||||
// Enhance returns the denoised audio.
|
||||
Enhance(ctx context.Context, req EnhancementRequest, opts ...EnhancementOption) (*SpeechResult, error)
|
||||
}
|
||||
|
||||
// SpeechEnhancerModelOption configures a SpeechEnhancer at construction time.
|
||||
// Reserved for future per-model settings.
|
||||
type SpeechEnhancerModelOption func(*SpeechEnhancerModelConfig)
|
||||
|
||||
// SpeechEnhancerModelConfig carries per-model construction settings.
|
||||
type SpeechEnhancerModelConfig struct{}
|
||||
|
||||
// ApplySpeechEnhancerModelOptions folds options into a config.
|
||||
func ApplySpeechEnhancerModelOptions(opts []SpeechEnhancerModelOption) SpeechEnhancerModelConfig {
|
||||
var cfg SpeechEnhancerModelConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SpeechEnhancementProvider mints SpeechEnhancers bound to one backend.
|
||||
type SpeechEnhancementProvider interface {
|
||||
// Name is the registry identifier for the provider.
|
||||
Name() string
|
||||
|
||||
// SpeechEnhancerModel returns a SpeechEnhancer bound to the given id
|
||||
// (passed through to the backend verbatim; no catalog validation).
|
||||
SpeechEnhancerModel(id string, opts ...SpeechEnhancerModelOption) (SpeechEnhancer, error)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package audio
|
||||
|
||||
import "context"
|
||||
|
||||
// StemSeparationRequest asks a source-separation backend (Demucs style) to
|
||||
// split a mix into stems. Audio is carried as bytes (never a URL), mirroring
|
||||
// TranscriptionRequest. Zero values mean "backend default" (ADR-0024).
|
||||
type StemSeparationRequest struct {
|
||||
// Audio is the encoded mix to separate.
|
||||
Audio []byte
|
||||
|
||||
// MIME is the audio MIME type (e.g. "audio/mpeg"); "" = let the backend
|
||||
// sniff it.
|
||||
MIME string
|
||||
|
||||
// Filename is the multipart filename hint some backends key their format
|
||||
// detection on; "" derives one from MIME or falls back to "audio".
|
||||
Filename string
|
||||
|
||||
// Mode selects the split: "two" (vocals + accompaniment) or "four"
|
||||
// (vocals/drums/bass/other); "" = backend default (four).
|
||||
Mode string
|
||||
|
||||
// Model selects the separator's internal network where the backend
|
||||
// offers several (Demucs: "htdemucs", "htdemucs_ft"); "" = backend
|
||||
// default. This is NOT the provider model id — that is fixed when the
|
||||
// StemSeparator is minted (mirrors BackgroundRemovalRequest.Net).
|
||||
Model string
|
||||
|
||||
// Format is the per-stem audio container ("mp3" or "wav");
|
||||
// "" = backend default.
|
||||
Format string
|
||||
}
|
||||
|
||||
// Stem is one separated source.
|
||||
type Stem struct {
|
||||
// Name is the stem's name ("vocals", "drums", "bass", "other",
|
||||
// "no_vocals", ...), taken from the backend's own labelling.
|
||||
Name string
|
||||
|
||||
// Audio is the encoded stem.
|
||||
Audio []byte
|
||||
|
||||
// MIME is the stem's audio MIME type, e.g. "audio/mpeg".
|
||||
MIME string
|
||||
}
|
||||
|
||||
// StemSeparationResult is the canonical separation result.
|
||||
type StemSeparationResult struct {
|
||||
// Stems are the separated sources, in the order the backend returned
|
||||
// them.
|
||||
Stems []Stem
|
||||
}
|
||||
|
||||
// StemSeparationOption mutates a StemSeparationRequest before it is sent.
|
||||
type StemSeparationOption func(*StemSeparationRequest)
|
||||
|
||||
// WithStemMode selects the split ("two" or "four").
|
||||
func WithStemMode(m string) StemSeparationOption {
|
||||
return func(r *StemSeparationRequest) { r.Mode = m }
|
||||
}
|
||||
|
||||
// WithStemModel selects the separator's internal network.
|
||||
func WithStemModel(m string) StemSeparationOption {
|
||||
return func(r *StemSeparationRequest) { r.Model = m }
|
||||
}
|
||||
|
||||
// WithStemFormat sets the per-stem audio container ("mp3", "wav").
|
||||
func WithStemFormat(f string) StemSeparationOption {
|
||||
return func(r *StemSeparationRequest) { r.Format = f }
|
||||
}
|
||||
|
||||
// Apply returns a copy of the request with all options applied.
|
||||
func (r StemSeparationRequest) Apply(opts ...StemSeparationOption) StemSeparationRequest {
|
||||
for _, opt := range opts {
|
||||
opt(&r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// StemSeparator splits a mix into stems.
|
||||
type StemSeparator interface {
|
||||
// SeparateStems returns the separated sources. Separation is CPU-bound
|
||||
// and slow (minutes for a full song); bound the call with a context
|
||||
// deadline.
|
||||
SeparateStems(ctx context.Context, req StemSeparationRequest, opts ...StemSeparationOption) (*StemSeparationResult, error)
|
||||
}
|
||||
|
||||
// StemSeparatorModelOption configures a StemSeparator at construction time.
|
||||
// Reserved for future per-model settings.
|
||||
type StemSeparatorModelOption func(*StemSeparatorModelConfig)
|
||||
|
||||
// StemSeparatorModelConfig carries per-model construction settings.
|
||||
type StemSeparatorModelConfig struct{}
|
||||
|
||||
// ApplyStemSeparatorModelOptions folds options into a config.
|
||||
func ApplyStemSeparatorModelOptions(opts []StemSeparatorModelOption) StemSeparatorModelConfig {
|
||||
var cfg StemSeparatorModelConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// StemSeparationProvider mints StemSeparators bound to one backend.
|
||||
type StemSeparationProvider interface {
|
||||
// Name is the registry identifier for the provider.
|
||||
Name() string
|
||||
|
||||
// StemSeparatorModel returns a StemSeparator bound to the given id
|
||||
// (passed through to the backend verbatim; no catalog validation).
|
||||
StemSeparatorModel(id string, opts ...StemSeparatorModelOption) (StemSeparator, error)
|
||||
}
|
||||
Reference in New Issue
Block a user