- speakWithReference now validates the response IS audio via the same audioResultMIME sniff the sfx/enhance surfaces use — a 2xx JSON soft error or HTML proxy page was previously wrapped up as audio/wav bytes. - audioResultMIME moves to audio.go (next to speechMIME; it was defined in sfx.go but shared by enhance/clone) and learns the Ogg container normalization (application/ogg -> audio/ogg). - Stems zip unpack gains entry-count (16) and total-decompressed (1GB) caps on top of the existing per-entry cap — the per-entry bound alone still let a many-entry bomb multiply up. - sanitizeFilename drops NUL and both path separators too, so upload metadata can never smuggle directory structure to a file-writing shim. - New maxAudioResponseBytes (256MB) for bodies that ARE one audio clip (clone, enhance, sfx): a long WAV legitimately passes the 64MB JSON cap. - speakWithReference local renamed path -> upPath (naming parity with stems/enhance). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWCQcYStWXBYUy5sZWnbLT
62 lines
2.1 KiB
Go
62 lines
2.1 KiB
Go
// enhance.go implements audio.SpeechEnhancementProvider against a
|
|
// DeepFilterNet shim (audioutils) reached through llama-swap's /upstream
|
|
// passthrough (ADR-0024):
|
|
//
|
|
// POST /upstream/<id>/v1/enhance multipart file -> WAV
|
|
package llamaswap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/audio"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
)
|
|
|
|
// SpeechEnhancerModel implements audio.SpeechEnhancementProvider. The id
|
|
// selects which upstream llama-swap loads.
|
|
func (p *Provider) SpeechEnhancerModel(id string, opts ...audio.SpeechEnhancerModelOption) (audio.SpeechEnhancer, error) {
|
|
if err := p.requireBaseURL(); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = audio.ApplySpeechEnhancerModelOptions(opts)
|
|
return &speechEnhancerModel{p: p, id: id}, nil
|
|
}
|
|
|
|
type speechEnhancerModel struct {
|
|
p *Provider
|
|
id string
|
|
}
|
|
|
|
// Enhance implements audio.SpeechEnhancer. The endpoint always answers WAV.
|
|
func (m *speechEnhancerModel) Enhance(ctx context.Context, req audio.EnhancementRequest, opts ...audio.EnhancementOption) (*audio.SpeechResult, error) {
|
|
req = req.Apply(opts...)
|
|
if len(req.Audio) == 0 {
|
|
return nil, fmt.Errorf("%w: speech enhancement requires audio bytes", llm.ErrUnsupported)
|
|
}
|
|
path, err := upstreamPath(m.id, "/v1/enhance")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, contentType, err := buildMultipart("build enhance form",
|
|
filePart{field: "file", filename: transcriptionFilename(req.Filename, req.MIME), data: req.Audio},
|
|
nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, respType, err := m.p.doRaw(ctx, http.MethodPost, path, m.id, contentType, body, maxAudioResponseBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(raw) == 0 {
|
|
return nil, &llm.APIError{Provider: m.p.name, Model: m.id, Message: "enhance response contained no audio"}
|
|
}
|
|
mimeType := audioResultMIME(respType, raw)
|
|
if mimeType == "" {
|
|
return nil, &llm.APIError{Provider: m.p.name, Model: m.id,
|
|
Message: fmt.Sprintf("enhance response is not audio (Content-Type %q): %s", respType, truncateForError(raw))}
|
|
}
|
|
return &audio.SpeechResult{Audio: raw, MIME: mimeType}, nil
|
|
}
|