mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-17 13:10:23 -04:00
feat(audio-transform): add LocalVQE backend, bidi gRPC RPC, Studio UI
Introduce a generic "audio transform" capability for any audio-in / audio-out
operation (echo cancellation, noise suppression, dereverberation, voice
conversion, etc.) and ship LocalVQE as the first backend implementation.
Backend protocol:
- Two new gRPC RPCs in backend.proto: unary AudioTransform for batch and
bidirectional AudioTransformStream for low-latency frame-by-frame use.
This is the first bidi stream in the proto; per-frame unary at LocalVQE's
16 ms hop would be RTT-bound. Wire it through pkg/grpc/{client,server,
embed,interface,base} with paired-channel ergonomics.
LocalVQE backend (backend/go/localvqe/):
- Go-Purego wrapper around upstream liblocalvqe.so. CMake builds the upstream
shared lib + its libggml-cpu-*.so runtime variants directly — no MODULE
wrapper needed because LocalVQE handles CPU feature selection internally
via GGML_BACKEND_DL.
- Sets GGML_NTHREADS from opts.Threads (or runtime.NumCPU()-1) — without it
LocalVQE runs single-threaded at ~1× realtime instead of the documented
~9.6×.
- Reference-length policy: zero-pad short refs, truncate long ones (the
trailing portion can't have leaked into a mic that wasn't recording).
- Ginkgo test suite (9 always-on specs + 2 model-gated).
HTTP layer:
- POST /audio/transformations (alias /audio/transform): multipart batch
endpoint, accepts audio + optional reference + params[*]=v form fields.
Persists inputs alongside the output in GeneratedContentDir/audio so the
React UI history can replay past (audio, reference, output) triples.
- GET /audio/transformations/stream: WebSocket bidi, 16 ms PCM frames
(interleaved stereo mic+ref in, mono out). JSON session.update envelope
for config; constants hoisted in core/schema/audio_transform.go.
- ffmpeg-based input normalisation to 16 kHz mono s16 WAV via the existing
utils.AudioToWav (with passthrough fast-path), so the user can upload any
format / rate without seeing the model's strict 16 kHz constraint.
- BackendTraceAudioTransform integration so /api/backend-traces and the
Traces UI light up with audio_snippet base64 and timing.
- Routes registered under routes/localai.go (LocalAI extension; OpenAI has
no /audio/transformations endpoint), traced via TraceMiddleware.
Auth + capability + importer:
- FLAG_AUDIO_TRANSFORM (model_config.go), FeatureAudioTransform (default-on,
in APIFeatures), three RouteFeatureRegistry rows.
- localvqe added to knownPrefOnlyBackends with modality "audio-transform".
- Gallery entry localvqe-v1-1.3m (sha256-pinned, hosted on
huggingface.co/LocalAI-io/LocalVQE).
React UI:
- New /app/transform page surfaced via a dedicated "Enhance" sidebar
section (sibling of Tools / Biometrics) — the page is enhancement, not
generation, so it lives outside Studio. Two AudioInput components
(Upload + Record tabs, drag-drop, mic capture).
- Echo-test button: records mic while playing the loaded reference through
the speakers — the mic naturally picks up speaker bleed, giving a real
(mic, ref) pair for AEC testing without leaving the UI.
- Reusable WaveformPlayer (canvas peaks + click-to-seek + audio controls)
and useAudioPeaks hook (shared module-scoped AudioContext to avoid
hitting browser context limits with three players on one page); migrated
TTS, Sound, Traces audio blocks to use it.
- Past runs saved in localStorage via useMediaHistory('audio-transform') —
the history entry stores all three URLs so clicking re-renders the full
triple, not just the output.
Build + e2e:
- 11 matrix entries removed from .github/workflows/backend.yml (CUDA, ROCm,
SYCL, Metal, L4T): upstream supports only CPU + Vulkan, so we ship those
two and let GPU-class hardware route through Vulkan in the gallery
capabilities map.
- tests-localvqe-grpc-transform job in test-extra.yml (gated on
detect-changes.outputs.localvqe).
- New audio_transform capability + 4 specs in tests/e2e-backends.
- Playwright spec suite in core/http/react-ui/e2e/audio-transform.spec.js
(8 specs covering tabs, file upload, multipart shape, history, errors).
Docs:
- New docs/content/features/audio-transform.md covering the (audio,
reference) mental model, batch + WebSocket wire formats, LocalVQE param
keys, and a YAML config example. Cross-links from text-to-audio and
audio-to-text feature pages.
Assisted-by: Claude:claude-opus-4-7 [Bash Read Edit Write Agent TaskCreate]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
1024 lines
22 KiB
Go
1024 lines
22 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const maxGRPCMessageSize = 50 * 1024 * 1024 // 50MB
|
|
|
|
// bearerToken implements credentials.PerRPCCredentials to inject a bearer token
|
|
// into every gRPC call.
|
|
type bearerToken struct {
|
|
token string
|
|
}
|
|
|
|
func (b bearerToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
|
return map[string]string{"authorization": "Bearer " + b.token}, nil
|
|
}
|
|
|
|
func (b bearerToken) RequireTransportSecurity() bool { return false }
|
|
|
|
type Client struct {
|
|
address string
|
|
busy bool
|
|
parallel bool
|
|
token string
|
|
sync.Mutex
|
|
opMutex sync.Mutex
|
|
wd WatchDog
|
|
}
|
|
|
|
type WatchDog interface {
|
|
Mark(address string)
|
|
UnMark(address string)
|
|
}
|
|
|
|
func (c *Client) IsBusy() bool {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.busy
|
|
}
|
|
|
|
func (c *Client) setBusy(v bool) {
|
|
c.Lock()
|
|
c.busy = v
|
|
c.Unlock()
|
|
}
|
|
|
|
func (c *Client) wdMark() {
|
|
if c.wd != nil {
|
|
c.wd.Mark(c.address)
|
|
}
|
|
}
|
|
|
|
func (c *Client) wdUnMark() {
|
|
if c.wd != nil {
|
|
c.wd.UnMark(c.address)
|
|
}
|
|
}
|
|
|
|
// dial creates a gRPC client connection with common options.
|
|
// If c.token is set, bearer token credentials are included.
|
|
func (c *Client) dial() (*grpc.ClientConn, error) {
|
|
opts := []grpc.DialOption{
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithDefaultCallOptions(
|
|
grpc.MaxCallRecvMsgSize(maxGRPCMessageSize),
|
|
grpc.MaxCallSendMsgSize(maxGRPCMessageSize),
|
|
),
|
|
}
|
|
if c.token != "" {
|
|
opts = append(opts, grpc.WithPerRPCCredentials(bearerToken{token: c.token}))
|
|
}
|
|
return grpc.NewClient(c.address, opts...)
|
|
}
|
|
|
|
func (c *Client) HealthCheck(ctx context.Context) (bool, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
// The healthcheck call shouldn't take long time
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
res, err := client.Health(ctx, &pb.HealthMessage{})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if string(res.Message) == "OK" {
|
|
return true, nil
|
|
}
|
|
|
|
return false, fmt.Errorf("health check failed: %s", res.Message)
|
|
}
|
|
|
|
func (c *Client) Embeddings(ctx context.Context, in *pb.PredictOptions, opts ...grpc.CallOption) (*pb.EmbeddingResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
return client.Embedding(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) Predict(ctx context.Context, in *pb.PredictOptions, opts ...grpc.CallOption) (*pb.Reply, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
return client.Predict(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) LoadModel(ctx context.Context, in *pb.ModelOptions, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.LoadModel(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) PredictStream(ctx context.Context, in *pb.PredictOptions, f func(reply *pb.Reply), opts ...grpc.CallOption) error {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
stream, err := client.PredictStream(ctx, in, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
// Check if context is cancelled before receiving
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
reply, err := stream.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
// Check if error is due to context cancellation
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
fmt.Println("Error", err)
|
|
|
|
return err
|
|
}
|
|
f(reply)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) GenerateImage(ctx context.Context, in *pb.GenerateImageRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.GenerateImage(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) GenerateVideo(ctx context.Context, in *pb.GenerateVideoRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.GenerateVideo(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) TTS(ctx context.Context, in *pb.TTSRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.TTS(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) TTSStream(ctx context.Context, in *pb.TTSRequest, f func(reply *pb.Reply), opts ...grpc.CallOption) error {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
stream, err := client.TTSStream(ctx, in, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
// Check if context is cancelled before receiving
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
reply, err := stream.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
// Check if error is due to context cancellation
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return err
|
|
}
|
|
f(reply)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) SoundGeneration(ctx context.Context, in *pb.SoundGenerationRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.SoundGeneration(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) AudioTranscription(ctx context.Context, in *pb.TranscriptRequest, opts ...grpc.CallOption) (*pb.TranscriptResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.AudioTranscription(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) AudioTranscriptionStream(ctx context.Context, in *pb.TranscriptRequest, f func(chunk *pb.TranscriptStreamResponse), opts ...grpc.CallOption) error {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
stream, err := client.AudioTranscriptionStream(ctx, in, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
chunk, err := stream.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return err
|
|
}
|
|
f(chunk)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) TokenizeString(ctx context.Context, in *pb.PredictOptions, opts ...grpc.CallOption) (*pb.TokenizationResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
res, err := client.TokenizeString(ctx, in, opts...)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (c *Client) Status(ctx context.Context) (*pb.StatusResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.Status(ctx, &pb.HealthMessage{})
|
|
}
|
|
|
|
func (c *Client) StoresSet(ctx context.Context, in *pb.StoresSetOptions, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StoresSet(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) StoresDelete(ctx context.Context, in *pb.StoresDeleteOptions, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StoresDelete(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) StoresGet(ctx context.Context, in *pb.StoresGetOptions, opts ...grpc.CallOption) (*pb.StoresGetResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StoresGet(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) StoresFind(ctx context.Context, in *pb.StoresFindOptions, opts ...grpc.CallOption) (*pb.StoresFindResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StoresFind(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) Rerank(ctx context.Context, in *pb.RerankRequest, opts ...grpc.CallOption) (*pb.RerankResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.Rerank(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) GetTokenMetrics(ctx context.Context, in *pb.MetricsRequest, opts ...grpc.CallOption) (*pb.MetricsResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.GetMetrics(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) VAD(ctx context.Context, in *pb.VADRequest, opts ...grpc.CallOption) (*pb.VADResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.VAD(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) Detect(ctx context.Context, in *pb.DetectOptions, opts ...grpc.CallOption) (*pb.DetectResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.Detect(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) FaceVerify(ctx context.Context, in *pb.FaceVerifyRequest, opts ...grpc.CallOption) (*pb.FaceVerifyResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.FaceVerify(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) FaceAnalyze(ctx context.Context, in *pb.FaceAnalyzeRequest, opts ...grpc.CallOption) (*pb.FaceAnalyzeResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.FaceAnalyze(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) VoiceVerify(ctx context.Context, in *pb.VoiceVerifyRequest, opts ...grpc.CallOption) (*pb.VoiceVerifyResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.VoiceVerify(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) VoiceAnalyze(ctx context.Context, in *pb.VoiceAnalyzeRequest, opts ...grpc.CallOption) (*pb.VoiceAnalyzeResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.VoiceAnalyze(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) VoiceEmbed(ctx context.Context, in *pb.VoiceEmbedRequest, opts ...grpc.CallOption) (*pb.VoiceEmbedResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.VoiceEmbed(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) AudioEncode(ctx context.Context, in *pb.AudioEncodeRequest, opts ...grpc.CallOption) (*pb.AudioEncodeResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.AudioEncode(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) AudioDecode(ctx context.Context, in *pb.AudioDecodeRequest, opts ...grpc.CallOption) (*pb.AudioDecodeResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.AudioDecode(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) AudioTransform(ctx context.Context, in *pb.AudioTransformRequest, opts ...grpc.CallOption) (*pb.AudioTransformResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.AudioTransform(ctx, in, opts...)
|
|
}
|
|
|
|
// AudioTransformStreamClient is the duplex interface returned by
|
|
// (*Client).AudioTransformStream. Wraps the generated bidi client without
|
|
// leaking the proto package across the public boundary.
|
|
type AudioTransformStreamClient interface {
|
|
Send(*pb.AudioTransformFrameRequest) error
|
|
Recv() (*pb.AudioTransformFrameResponse, error)
|
|
CloseSend() error
|
|
Context() context.Context
|
|
}
|
|
|
|
// audioTransformStreamClient is the concrete wrapper. It also owns the
|
|
// underlying gRPC connection so it can be closed when the caller is done.
|
|
type audioTransformStreamClient struct {
|
|
pb.Backend_AudioTransformStreamClient
|
|
conn *grpc.ClientConn
|
|
closer func()
|
|
}
|
|
|
|
func (s *audioTransformStreamClient) CloseSend() error {
|
|
err := s.Backend_AudioTransformStreamClient.CloseSend()
|
|
if s.closer != nil {
|
|
s.closer()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *Client) AudioTransformStream(ctx context.Context, opts ...grpc.CallOption) (AudioTransformStreamClient, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
}
|
|
c.setBusy(true)
|
|
c.wdMark()
|
|
|
|
cleanup := func() {
|
|
c.wdUnMark()
|
|
c.setBusy(false)
|
|
if !c.parallel {
|
|
c.opMutex.Unlock()
|
|
}
|
|
}
|
|
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
cleanup()
|
|
return nil, err
|
|
}
|
|
client := pb.NewBackendClient(conn)
|
|
stream, err := client.AudioTransformStream(ctx, opts...)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
cleanup()
|
|
return nil, err
|
|
}
|
|
return &audioTransformStreamClient{
|
|
Backend_AudioTransformStreamClient: stream,
|
|
conn: conn,
|
|
closer: func() {
|
|
_ = conn.Close()
|
|
cleanup()
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) StartFineTune(ctx context.Context, in *pb.FineTuneRequest, opts ...grpc.CallOption) (*pb.FineTuneJobResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StartFineTune(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) FineTuneProgress(ctx context.Context, in *pb.FineTuneProgressRequest, f func(update *pb.FineTuneProgressUpdate), opts ...grpc.CallOption) error {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
stream, err := client.FineTuneProgress(ctx, in, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
update, err := stream.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return err
|
|
}
|
|
f(update)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) StopFineTune(ctx context.Context, in *pb.FineTuneStopRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StopFineTune(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) ListCheckpoints(ctx context.Context, in *pb.ListCheckpointsRequest, opts ...grpc.CallOption) (*pb.ListCheckpointsResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.ListCheckpoints(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) ExportModel(ctx context.Context, in *pb.ExportModelRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.ExportModel(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) StartQuantization(ctx context.Context, in *pb.QuantizationRequest, opts ...grpc.CallOption) (*pb.QuantizationJobResult, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StartQuantization(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) QuantizationProgress(ctx context.Context, in *pb.QuantizationProgressRequest, f func(update *pb.QuantizationProgressUpdate), opts ...grpc.CallOption) error {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
|
|
stream, err := client.QuantizationProgress(ctx, in, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
update, err := stream.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return err
|
|
}
|
|
f(update)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) StopQuantization(ctx context.Context, in *pb.QuantizationStopRequest, opts ...grpc.CallOption) (*pb.Result, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.StopQuantization(ctx, in, opts...)
|
|
}
|
|
|
|
func (c *Client) Free(ctx context.Context) error {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := pb.NewBackendClient(conn)
|
|
_, err = client.Free(ctx, &pb.HealthMessage{})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) ModelMetadata(ctx context.Context, in *pb.ModelOptions, opts ...grpc.CallOption) (*pb.ModelMetadataResponse, error) {
|
|
if !c.parallel {
|
|
c.opMutex.Lock()
|
|
defer c.opMutex.Unlock()
|
|
}
|
|
c.setBusy(true)
|
|
defer c.setBusy(false)
|
|
c.wdMark()
|
|
defer c.wdUnMark()
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := pb.NewBackendClient(conn)
|
|
return client.ModelMetadata(ctx, in, opts...)
|
|
}
|