Files
ragflow/internal/agent/audio/tts_design.md
Zhichang Yu e45659868a feat(agent): ship the Go agent canvas port — eino interrupt/resume + Redis check-pointing (#16035)
Replaces the Python agent canvas runtime with a Go implementation that
runs inside `cmd/server_main`.

The canvas compiles into an eino Workflow that pauses on wait-for-user
via native Interrupt/Resume (no sentinel flag) and resumes from a
Redis-backed CheckPointStore.

All 21 Python agent components and ~35 tools are ported with functional
parity.

Sandbox providers now read their JSON config from the admin-panel
system_settings table with env fallback.

234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate
added); drops the v2 DSL E2E step and the gap-analysis plan (both
redundant after the port ships).

## Type of change

- [x] Refactoring
- [x] New feature
- [x] Bug fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 13:24:03 +08:00

155 lines
6.1 KiB
Markdown

# TTS engine — Phase 8b design decision
Status: **decision recorded, implementation pending**. The TTS
scaffold exists (internal/agent/audio/tts.go); this doc records
how a real engine should be wired in. The current scaffold's
shellSynthesizer uses an invented `--engine --text --voice --lang`
protocol that no real TTS binary matches — it must not be used
as-is.
## Context (what the Python side actually does)
The Python `agent/canvas.py:518-521` does NOT use `gtts` or
`edge-tts` shell-out. It looks up the tenant's default TTS model
via `get_tenant_default_model_by_type(self._tenant_id, LLMType.TTS)`
and creates an `LLMBundle(tenant, tts_model_config)`. The TTS
factory in `rag/llm/tts_model.py` dispatches to one of several
**HTTP-based** providers:
| Provider | Backend | Pure Go? |
|----------|---------|----------|
| FishAudioTTS | HTTP POST to `api.fish.audio/v1/tts` (msgpack body) | feasible |
| QwenTTS | DashScope SDK (over WebSocket) | heavy |
| OpenAITTS | HTTP POST to OpenAI-compatible `/audio/tts` | feasible |
| StepFunTTS | HTTP POST to vendor endpoint | feasible |
| RAGconTTS | LiteLLM proxy (HTTP) | feasible |
| XinferenceTTS | HTTP POST to xinference | feasible |
| TongyiTTS | DashScope SDK | heavy |
None of the production providers are gtts / edge-tts. The "gtts or
edge-tts shell-out" wording in the original plan was a placeholder
that didn't survive the v3.1 review; the production TTS layer is
HTTP / SDK all the way down.
## Options for the Go port
### A. Reimplement the HTTP clients in Go
Write Go HTTP clients for each provider (Fish / OpenAI / StepFun
/ Xinference / LiteLLM-proxy). Skip the DashScope SDK variants
for now (Qwen, Tongyi) — those are websocket-heavy and can
follow in a later phase.
Pros
: No Python dependency on the Go side.
: Lower latency, no subprocess overhead per call.
: Clean integration with the rest of the Go runtime.
Cons
: Five HTTP surfaces to maintain in lockstep with the Python
ones. Every vendor release needs a Go update.
: The Python TTS layer is part of a multi-tenant
`LLMBundle` abstraction (config, key rotation, retry
policy) — reimplementing just the wire layer loses those
cross-cutting concerns.
### B. Shell out to a Python subprocess that uses rag.llm.tts_model
Spawn `python3 -c "from rag.llm.tts_model import ...; ..."` and
pipe the audio bytes back.
Pros
: Reuses the verified Python TTS layer verbatim — including
all providers, the LLMBundle config / key handling, and the
retry / streaming logic.
: Plan §2.11.4-style "don't rewrite the vendor layer" applies
here too: rag/llm/tts_model.py IS the vendor layer.
: One Python subprocess call covers all current providers.
Cons
: Per-call latency = Python interpreter startup + TTS module
import + HTTP to vendor. ~hundreds of ms.
: Adds a Python dependency on the Go host.
: Stream chunks back from Python are awkward (binary audio).
### C. Hybrid — reimplement the simple HTTP ones, shell out for the rest
Reimplement OpenAI / Fish / Xinference / StepFun / LiteLLM-proxy
(5 providers, all straightforward HTTP). Shell out only for the
DashScope-SDK providers (Qwen, Tongyi) — those are websocket
clients whose cost / benefit doesn't justify a reimplementation
until the Go side has more DashScope users.
Pros
: Common case (OpenAI-compatible / LiteLLM proxy) has no
Python dep.
: Avoids the worst of option B (latency + Python dep) for
the providers most users actually deploy.
Cons
: Two code paths to maintain (Go HTTP + Python SDK).
: DashScope providers are exactly the ones several Chinese
RAGFlow operators use — leaving them on the Python
fallback is a real capability gap.
## Decision
**Option A (reimplement the HTTP clients in Go).** Reasoning:
1. All non-DashScope providers are straightforward HTTP POSTs
with JSON / msgpack bodies. None of them need a streaming
audio reader that's complex enough to justify a Python
subprocess call.
2. The Go `rag/llm` package already has the same factory
pattern for chat / embedding / rerank models. TTS slots
into the same plumbing.
3. Option B's latency cost (~hundreds of ms for a Python
interpreter + module import) is wasted — audio synthesis
takes seconds anyway; the Python-startup overhead is
noise compared to the network round-trip.
4. Option C's hybrid is the worst of both worlds: a new
Go codebase that has to keep parity with a Python fallback
for the DashScope providers.
For the DashScope SDK providers (Qwen, Tongyi), we accept
"not yet supported in Go" — the user can fall back to the
Python Canvas. Loud-fail via the existing
`ErrTTSEngineNotConfigured` pattern.
## Implementation sketch
1. New file `internal/rag/llm/tts_model.go` (Go side) with:
- `TTSModel` interface: `Synthesize(ctx, text) (io.Reader, error)`
- `FishTTS`, `OpenAITTS`, `XinferenceTTS`, `StepFunTTS`,
`LiteLLMProxyTTS` implementations.
- `TTSFactory` registry mirroring the Python one.
2. Wire `audio.Synthesizer` in the existing scaffold to
delegate to `rag/llm/tts_model` (replacing the invented
shell-out protocol in the current scaffold).
3. New file `internal/rag/llm/tts_model_test.go` with HTTP
mock tests for each provider's request shape.
## What the existing scaffold needs to change
The current `shellSynthesizer.Synthesize` uses
`--engine --text --voice --lang` argv — no real TTS binary
matches that. Until option A lands, the existing scaffold
should be considered a placeholder. A subsequent commit
should either:
- (a) delete `shellSynthesizer` and `InstallShellSynthesizer`,
leaving only the stub that returns
`ErrTTSEngineNotConfigured`, or
- (b) replace them with a thin Python-subprocess client
using the proven `rag.llm.tts_model` entry point (a
safe interim — same pattern as CodeExec).
Option (a) is the safer default: it removes a footgun
(a non-functional shell-out) and lets the operator
discover the deferred state through the standard
"ErrTTSEngineNotConfigured" error. Option (b) is the
"ship a working path today" choice.
The choice between (a) and (b) is left to the implementer
of option A; until then, the scaffold's `shellSynthesizer`
is dead code.