Decoder chraac en sous-process : -20% decoder, -9% pipeline, talker intact
Architecture pour exploiter le gain chraac sans casser le talker :
- kazeia_decoder_chraac : binaire static contre libqwen3tts-decoder.a chraac
+ ggml chraac statiques (9.8 MB autonome, pas de dép .so chraac).
Args : <decoder.gguf> <codes.bin> <T> <out.wav>. Lit codes int32 time-major,
transpose codebook-major, forward, écrit WAV.
- tts_engine : si KZTTS_DECODER_SUBPROC=path + KZTTS_DECODER_GGUF=path posés,
fork+execl au lieu de dec.forward intégré. Codes via /data/local/tmp tmpfile,
WAV via rename. Le talker+CP continuent sur ql/ggml (qualcomm) qui marche.
Mesures Pad3 KZTTS_THREADS=6 GGML_NUM_THREADS=8 seed=42 :
Phrase 'Bonjour Kazeia' (33 frames) :
A baseline qualcomm : decoder 3.29s, total 6.61s, RTF 2.40
B chraac subproc : decoder 2.63s, total 6.06s, RTF 2.20
-> decoder -20%, total -8.3%, audio RMS 1743 identique
Phrase 'Bonsoir, comment tu te sens ce soir ?' (41 frames) :
A baseline : decoder 3.95s, total 8.16s, RTF 2.39
B subproc : decoder 3.15s, total 7.38s, RTF 2.16
-> decoder -20%, total -9.6%
Audio cohérent au baseline (range, RMS quasi identiques), imperceptible
à l'oreille (cf cas 'Bonsoir' envoyé).
Le gain chraac historique sur decoder seul est désormais accessible SANS
casser le talker.
Code committé opt-in (KZTTS_DECODER_SUBPROC absent par défaut = baseline intact).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2809b54943
commit
c332943181
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build kazeia_decoder_chraac : sous-process décodeur statique chraac.
|
||||
# Link entièrement statique contre les .a chraac (libggml + libqwen3tts-decoder)
|
||||
# pour être autonome — aucun .so chraac sur la tablette n'est requis au runtime.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
NDK="${ANDROID_NDK_ROOT:-/opt/Kazeia/android-ndk-r27d}"
|
||||
CXX="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++"
|
||||
|
||||
DEC_BUILD="/opt/Kazeia/kazeia-tts-decoder-ggml/build-android-chraac-fresh"
|
||||
DEC_SRC="/opt/Kazeia/kazeia-tts-decoder-ggml/src"
|
||||
CHRAAC_GGML_INC="/opt/Kazeia/chraac-llama/ggml/include"
|
||||
|
||||
[ -f "$DEC_BUILD/libqwen3tts-decoder.a" ] || { echo "decoder.a chraac absent: $DEC_BUILD"; exit 1; }
|
||||
|
||||
mkdir -p "$HERE/b-jni"
|
||||
OUT="$HERE/b-jni/kazeia_decoder_chraac"
|
||||
|
||||
CFLAGS=( -std=c++17 -O3 -fPIE
|
||||
-march=armv8.6-a+i8mm+bf16+dotprod+fp16
|
||||
-Wno-unused-parameter -Wno-unused-variable -Wno-sign-compare
|
||||
)
|
||||
INCS=( -I"$DEC_SRC" -I"$CHRAAC_GGML_INC" )
|
||||
|
||||
# Important : --start-group / --end-group pour résoudre les références cycliques
|
||||
# entre libqwen3tts-decoder et ggml. Ordre: decoder.a en premier (références
|
||||
# ggml symbols), puis ggml-cpu, ggml-base, ggml (qui se référencent entre eux).
|
||||
LIBS=(
|
||||
"$HERE/jni/decoder_subprocess.cpp"
|
||||
-Wl,--start-group
|
||||
"$DEC_BUILD/libqwen3tts-decoder.a"
|
||||
"$DEC_BUILD/ggml-build/src/libggml-cpu.a"
|
||||
"$DEC_BUILD/ggml-build/src/libggml-base.a"
|
||||
"$DEC_BUILD/ggml-build/src/libggml.a"
|
||||
-Wl,--end-group
|
||||
-fopenmp -static-openmp
|
||||
-llog -ldl -lm
|
||||
)
|
||||
|
||||
echo "== building $OUT =="
|
||||
"$CXX" "${CFLAGS[@]}" "${INCS[@]}" "${LIBS[@]}" -o "$OUT"
|
||||
ls -la "$OUT"
|
||||
echo "== check qu'il n'y a pas de dépendance .so chraac =="
|
||||
"$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" -d "$OUT" | grep -E "NEEDED|RUNPATH"
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Sous-process décodeur TTS standalone, linké STATIQUE contre chraac-llama/ggml
|
||||
// (RTF decoder 0.96 historique vs 1.47 qualcomm fork). Utilisé par tts_engine
|
||||
// quand KZTTS_DECODER_SUBPROC=path/kazeia_decoder_chraac est posé.
|
||||
//
|
||||
// Le talker (sensible aux différences llama.cpp entre forks) reste dans le process
|
||||
// principal sur ql/ggml (qualcomm). Le decoder ici tourne dans un process séparé
|
||||
// linké static chraac → pas de mismatch ABI dans le binaire principal.
|
||||
//
|
||||
// Usage : kazeia_decoder_chraac <decoder.gguf> <codes.bin> <T> <out.wav>
|
||||
// codes.bin : int32 [T, 16] time-major (= ce que tts_engine produit)
|
||||
//
|
||||
// Communication via FICHIERS (codes binaires en input, WAV en output). Simple,
|
||||
// debuggable. Pour perf future : pipe stdin/stdout ou shared memory.
|
||||
#include "decoder.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
using namespace kazeia::tts;
|
||||
|
||||
// Copié de tts_engine.cpp (libqwen3tts-decoder.a ne l'expose pas).
|
||||
static bool write_wav_pcm16_mono(const char* path, const float* samples, size_t N, int sr = 24000) {
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) return false;
|
||||
const uint32_t data_bytes = (uint32_t)(N * 2);
|
||||
const uint32_t riff_size = 36 + data_bytes;
|
||||
auto w16 = [&](uint16_t v){ fwrite(&v, 2, 1, f); };
|
||||
auto w32 = [&](uint32_t v){ fwrite(&v, 4, 1, f); };
|
||||
fwrite("RIFF", 1, 4, f); w32(riff_size); fwrite("WAVE", 1, 4, f);
|
||||
fwrite("fmt ", 1, 4, f); w32(16); w16(1); w16(1);
|
||||
w32((uint32_t)sr); w32((uint32_t)sr * 2); w16(2); w16(16);
|
||||
fwrite("data", 1, 4, f); w32(data_bytes);
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
float v = samples[i]; if (v > 1.f) v = 1.f; else if (v < -1.f) v = -1.f;
|
||||
int16_t pcm = (int16_t)std::lround(v * 32767.0f);
|
||||
fwrite(&pcm, 2, 1, f);
|
||||
}
|
||||
fclose(f); return true;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 5) {
|
||||
fprintf(stderr, "usage: %s <decoder.gguf> <codes.bin> <T> <out.wav>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
const char* model_path = argv[1];
|
||||
const char* codes_path = argv[2];
|
||||
const int T = atoi(argv[3]);
|
||||
const char* out_wav = argv[4];
|
||||
|
||||
// Read codes time-major [T, 16]
|
||||
std::ifstream f(codes_path, std::ios::binary | std::ios::ate);
|
||||
if (!f) { fprintf(stderr, "open %s FAIL\n", codes_path); return 2; }
|
||||
const size_t n_bytes = (size_t)f.tellg();
|
||||
const size_t n_expected = (size_t)T * 16 * sizeof(int32_t);
|
||||
if (n_bytes != n_expected) {
|
||||
fprintf(stderr, "codes size mismatch: got %zu bytes, expected %zu (T=%d)\n",
|
||||
n_bytes, n_expected, T);
|
||||
return 3;
|
||||
}
|
||||
std::vector<int32_t> codes_time_major(T * 16);
|
||||
f.seekg(0);
|
||||
f.read((char*)codes_time_major.data(), n_expected);
|
||||
f.close();
|
||||
|
||||
// Transpose [T, 16] -> [16, T] (codebook-major attendu par decoder.forward)
|
||||
std::vector<int32_t> codes_dec(16 * T);
|
||||
for (int t = 0; t < T; ++t)
|
||||
for (int c = 0; c < 16; ++c)
|
||||
codes_dec[c * T + t] = codes_time_major[t * 16 + c];
|
||||
|
||||
// Load decoder + forward
|
||||
Decoder dec;
|
||||
if (!dec.load(model_path)) {
|
||||
fprintf(stderr, "decoder.load(%s) FAIL\n", model_path);
|
||||
return 4;
|
||||
}
|
||||
auto wav = dec.forward(codes_dec, T);
|
||||
|
||||
if (!write_wav_pcm16_mono(out_wav, wav.data(), wav.size(), 24000)) {
|
||||
fprintf(stderr, "write WAV %s FAIL\n", out_wav);
|
||||
return 5;
|
||||
}
|
||||
fprintf(stderr, "kazeia_decoder_chraac OK: T=%d -> %zu samples -> %s\n",
|
||||
T, wav.size(), out_wav);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@
|
|||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace kazeia::tts;
|
||||
|
||||
|
|
@ -612,6 +614,45 @@ TtsSynthesizeResult tts_engine_synthesize(TtsEngine * eng, const TtsSynthesizeCf
|
|||
R.audio_s = N_done / 12.0;
|
||||
|
||||
// --- 8) Decoder final -> WAV ---
|
||||
// KZTTS_DECODER_SUBPROC=path/kazeia_decoder_chraac : fork+exec un sous-process décodeur
|
||||
// statiquement linké contre chraac (-22% sur decoder). Le talker reste qualcomm (qui marche).
|
||||
// Demande KZTTS_DECODER_GGUF=path/qwen3tts_decoder.gguf pour passer en argv au sous-process.
|
||||
const char * subproc_bin = getenv("KZTTS_DECODER_SUBPROC");
|
||||
const char * subproc_model = getenv("KZTTS_DECODER_GGUF");
|
||||
if (subproc_bin && subproc_model && stream_chunk == 0) {
|
||||
// Codes time-major [N_done, 16] -> fichier tmp
|
||||
char codes_tmp[256], wav_tmp[256], n_str[32];
|
||||
snprintf(codes_tmp, sizeof(codes_tmp), "/data/local/tmp/kazeia_codes_%d.bin", (int)getpid());
|
||||
snprintf(wav_tmp, sizeof(wav_tmp), "/data/local/tmp/kazeia_wav_%d.wav", (int)getpid());
|
||||
snprintf(n_str, sizeof(n_str), "%d", N_done);
|
||||
{
|
||||
FILE * f = fopen(codes_tmp, "wb");
|
||||
if (!f) { fprintf(stderr, "subproc: fopen codes FAIL\n"); R.err = -7; return R; }
|
||||
fwrite(codes_engine.data(), sizeof(int32_t), codes_engine.size(), f);
|
||||
fclose(f);
|
||||
}
|
||||
const double t_dec0 = now_s();
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
execl(subproc_bin, subproc_bin, subproc_model, codes_tmp, n_str, wav_tmp, (char*)nullptr);
|
||||
fprintf(stderr, "subproc execl FAIL\n");
|
||||
_exit(127);
|
||||
} else if (pid < 0) {
|
||||
fprintf(stderr, "subproc fork FAIL\n"); R.err = -8; return R;
|
||||
}
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
R.decoder_s = now_s() - t_dec0;
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
fprintf(stderr, "subproc exited code=%d\n", WEXITSTATUS(status)); R.err = -9; return R;
|
||||
}
|
||||
if (rename(wav_tmp, cfg.out_wav_path) != 0) {
|
||||
fprintf(stderr, "subproc rename WAV %s -> %s FAIL\n", wav_tmp, cfg.out_wav_path);
|
||||
R.err = -10; return R;
|
||||
}
|
||||
unlink(codes_tmp);
|
||||
R.n_chunks = 1;
|
||||
} else
|
||||
if (stream_chunk > 0) {
|
||||
// Mode streaming : flush le dernier chunk s'il reste des frames non décodées.
|
||||
if (last_decoded_N < N_done) run_chunk(N_done);
|
||||
|
|
|
|||
Loading…
Reference in New Issue