Files
LocalAI/backend/go/moss-tts-cpp/cpp/mossttscpp.cpp
LocalAI [bot] 3bb0d1cb49 feat(backend): add moss-tts-cpp text-to-speech backend (#10860)
* feat(backend): add moss-tts-cpp text-to-speech backend

Add a Go + purego backend wrapping the moss-tts.cpp ggml port of the OpenMOSS
MOSS-TTS-Local v1.5 text-to-speech model (GPT-J local transformer decoded through
MOSS-Audio-Tokenizer-v2), producing 48 kHz stereo audio with optional
reference-audio voice cloning. Mirrors the qwen3-tts-cpp backend: dlopen the
static-ggml shared library, bind the moss-tts.cpp C-API via purego, and serve
the gRPC TTS method. A thin C shim holds the pipeline handle and copies engine
PCM into a Go-freeable buffer.

Wires the CI registration: backend-matrix.yml (CPU, CUDA 12/13, Intel SYCL
f16/f32, Vulkan, ROCm, NVIDIA L4T, plus Darwin metal), backend/index.yaml metas
and image entries pointing at mudler/MOSS-TTS-Local-Transformer-v1.5-GGUF, the
root Makefile build targets, and the changed-backends.js path mapping.

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* docs: list the moss-tts-cpp backend among the LocalAI-maintained engines

Add moss-tts.cpp to the README "Backends built by us" table, the
Text-to-Speech compatibility table, and the reference-audio voice-cloning
backend list, so the new backend is documented alongside its peers.

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* backend(moss-tts-cpp): pin moss-tts.cpp to the squashed single-commit release

moss-tts.cpp history was collapsed to a single commit; repoint MOSSTTS_CPP_VERSION
to ee722b8e9205ee9b1b1c398a4e87e4e393e9be41.

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* backend(moss-tts-cpp): add the moss-tts-cpp-development gallery meta

The gallery had the -development image entries but no matching -development
meta anchor (as locate-anything-cpp and depth-anything-cpp have), so the master
build was not installable as a gallery backend. Add moss-tts-cpp-development
mirroring the production meta with the -development capability image names.

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-17 09:26:12 +02:00

120 lines
3.6 KiB
C++

#include "mossttscpp.h"
#include "ggml-backend.h"
#include "moss_tts_capi.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
// Single loaded Local pipeline. base.SingleThread serializes calls on the Go
// side, so a plain global is safe (mirrors qwen3-tts-cpp's g_ctx).
static moss_local *g_local = nullptr;
static void ggml_log_cb(enum ggml_log_level level, const char *log,
void * /*data*/) {
if (!log)
return;
const char *lvl = "?????";
switch (level) {
case GGML_LOG_LEVEL_DEBUG: lvl = "DEBUG"; break;
case GGML_LOG_LEVEL_INFO: lvl = "INFO"; break;
case GGML_LOG_LEVEL_WARN: lvl = "WARN"; break;
case GGML_LOG_LEVEL_ERROR: lvl = "ERROR"; break;
default: break;
}
fprintf(stderr, "[%-5s] %s", lvl, log);
fflush(stderr);
}
int mtl_load(const char *local_path, const char *codec_path,
const char *tokenizer_path) {
ggml_log_set(ggml_log_cb, nullptr);
ggml_backend_load_all();
if (!local_path || local_path[0] == '\0') {
fprintf(stderr, "[moss-tts-cpp] ERROR: local_path is required\n");
return 1;
}
if (!codec_path || codec_path[0] == '\0') {
fprintf(stderr, "[moss-tts-cpp] ERROR: codec_path is required\n");
return 2;
}
if (!tokenizer_path || tokenizer_path[0] == '\0') {
fprintf(stderr, "[moss-tts-cpp] ERROR: tokenizer_path is required\n");
return 3;
}
fprintf(stderr,
"[moss-tts-cpp] Loading local=%s codec=%s tokenizer=%s\n",
local_path, codec_path, tokenizer_path);
g_local = moss_local_load(local_path, codec_path, tokenizer_path);
if (!g_local) {
fprintf(stderr, "[moss-tts-cpp] FATAL: moss_local_load failed\n");
return 4;
}
fprintf(stderr, "[moss-tts-cpp] Model loaded (%s)\n", moss_tts_version());
return 0;
}
float *mtl_tts(const char *text, const char *reference_wav, int seed,
int *out_n, int *out_sr) {
if (out_n)
*out_n = 0;
if (out_sr)
*out_sr = 0;
if (!g_local) {
fprintf(stderr, "[moss-tts-cpp] ERROR: model not loaded\n");
return nullptr;
}
if (!text || text[0] == '\0') {
fprintf(stderr, "[moss-tts-cpp] ERROR: text is required\n");
return nullptr;
}
// An empty reference path means "no cloning": pass NULL so the engine skips
// the clone branch rather than trying to open "".
const char *ref = (reference_wav && reference_wav[0] != '\0')
? reference_wav
: nullptr;
int n = 0, sr = 0;
float *pcm = moss_local_tts(g_local, text, ref, seed, &n, &sr);
if (!pcm || n <= 0) {
fprintf(stderr, "[moss-tts-cpp] ERROR: moss_local_tts failed\n");
if (pcm)
moss_free(pcm);
return nullptr;
}
// Copy into a plain malloc buffer the Go side frees via mtl_pcm_free, then
// release the engine-owned buffer with moss_free (mirrors qwen3-tts-cpp,
// keeping ownership on the C runtime's malloc/free).
size_t bytes = (size_t)n * sizeof(float);
float *buf = (float *)malloc(bytes);
if (!buf) {
fprintf(stderr, "[moss-tts-cpp] ERROR: malloc(%zu) failed\n", bytes);
moss_free(pcm);
return nullptr;
}
memcpy(buf, pcm, bytes);
moss_free(pcm);
if (out_n)
*out_n = n;
if (out_sr)
*out_sr = sr;
return buf;
}
void mtl_pcm_free(float *p) { free(p); }
void mtl_unload(void) {
if (g_local) {
moss_local_free(g_local);
g_local = nullptr;
}
}
const char *mtl_version(void) { return moss_tts_version(); }