feat(multimodal): 视觉分层路由(多模态检测→主/视觉/OCR 工具)+ 语音 STT→TTS 降级链(异步)
This commit is contained in:
1 parent
02aae2bbb4
commit
0a936a3202
19 files changed
+2121
No files matched your search
@@ -0,0 +1,160 @@
|
||||
package voice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Service runs STT/TTS requests on a background worker pool so caller
|
||||
// goroutines (the agent loop) are never blocked by provider latency. Each
|
||||
// request is enqueued on a jobs channel and answered on a per-job result
|
||||
// channel; a canceled caller context or a closed service unblocks the caller
|
||||
// immediately and no worker goroutine is leaked. It is safe for concurrent use.
|
||||
type Service struct {
|
||||
chain *Chain
|
||||
jobs chan job
|
||||
bgCtx context.Context
|
||||
cancel context.CancelFunc
|
||||
nWorkers int
|
||||
wg sync.WaitGroup
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// ServiceOption customizes a Service.
|
||||
type ServiceOption func(*Service)
|
||||
|
||||
// WithWorkers sets the worker-pool size. Values <= 0 are ignored and the
|
||||
// default single worker is kept.
|
||||
func WithWorkers(n int) ServiceOption {
|
||||
return func(s *Service) {
|
||||
if n > 0 {
|
||||
s.nWorkers = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// jobKind selects the operation a worker performs.
|
||||
type jobKind int
|
||||
|
||||
const (
|
||||
jobSTT jobKind = iota
|
||||
jobTTS
|
||||
)
|
||||
|
||||
// job is one request enqueued by the caller.
|
||||
type job struct {
|
||||
kind jobKind
|
||||
audio Audio
|
||||
text string
|
||||
res chan jobResult
|
||||
}
|
||||
|
||||
// jobResult carries the outcome written back to the caller.
|
||||
type jobResult struct {
|
||||
text string
|
||||
audio Audio
|
||||
err error
|
||||
}
|
||||
|
||||
// NewService builds a Service backed by chain and starts the worker pool.
|
||||
func NewService(chain *Chain, opts ...ServiceOption) *Service {
|
||||
bg, cancel := context.WithCancel(context.Background())
|
||||
s := &Service{
|
||||
chain: chain,
|
||||
jobs: make(chan job),
|
||||
bgCtx: bg,
|
||||
cancel: cancel,
|
||||
nWorkers: 1,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(s)
|
||||
}
|
||||
for i := 0; i < s.nWorkers; i++ {
|
||||
s.wg.Add(1)
|
||||
go s.worker()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// worker consumes jobs until the service closes.
|
||||
func (s *Service) worker() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-s.bgCtx.Done():
|
||||
return
|
||||
case j := <-s.jobs:
|
||||
s.run(j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run executes a job and delivers the result. Providers run under the service's
|
||||
// background context so Close cancels in-flight work and no worker goroutine is
|
||||
// leaked; the result is written to a buffered channel so a caller that already
|
||||
// returned never blocks the worker.
|
||||
func (s *Service) run(j job) {
|
||||
res := jobResult{}
|
||||
switch j.kind {
|
||||
case jobSTT:
|
||||
res.text, res.err = s.chain.STT(s.bgCtx, j.audio)
|
||||
case jobTTS:
|
||||
res.audio, res.err = s.chain.TTS(s.bgCtx, j.text)
|
||||
}
|
||||
j.res <- res
|
||||
}
|
||||
|
||||
// STT enqueues a transcription request and waits for the result. ctx cancels
|
||||
// the wait and the underlying provider call.
|
||||
func (s *Service) STT(ctx context.Context, audio Audio) (string, error) {
|
||||
res, err := s.submit(ctx, job{kind: jobSTT, audio: audio, res: make(chan jobResult, 1)})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.text, res.err
|
||||
}
|
||||
|
||||
// TTS enqueues a synthesis request and waits for the result. ctx cancels the
|
||||
// wait and the underlying provider call.
|
||||
func (s *Service) TTS(ctx context.Context, text string) (Audio, error) {
|
||||
res, err := s.submit(ctx, job{kind: jobTTS, text: text, res: make(chan jobResult, 1)})
|
||||
if err != nil {
|
||||
return Audio{}, err
|
||||
}
|
||||
return res.audio, res.err
|
||||
}
|
||||
|
||||
// submit enqueues j and blocks until the result or a cancellation.
|
||||
func (s *Service) submit(ctx context.Context, j job) (jobResult, error) {
|
||||
select {
|
||||
case s.jobs <- j:
|
||||
case <-ctx.Done():
|
||||
return jobResult{}, fmt.Errorf("voice: %w", ctx.Err())
|
||||
case <-s.bgCtx.Done():
|
||||
return jobResult{}, ErrClosed
|
||||
}
|
||||
select {
|
||||
case r := <-j.res:
|
||||
return r, nil
|
||||
case <-ctx.Done():
|
||||
return jobResult{}, fmt.Errorf("voice: %w", ctx.Err())
|
||||
case <-s.bgCtx.Done():
|
||||
return jobResult{}, ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
// Close cancels the worker pool and waits for in-flight jobs to finish. It is
|
||||
// safe to call once; later calls are no-ops. In-flight provider calls are
|
||||
// bounded by each provider's own context timeout, so Close returns promptly.
|
||||
func (s *Service) Close() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.cancel()
|
||||
s.wg.Wait()
|
||||
}
|
||||
Reference in new issue
Block a user