diff --git a/dist/build_decoder_subproc.sh b/dist/build_decoder_subproc.sh new file mode 100755 index 0000000..489271a --- /dev/null +++ b/dist/build_decoder_subproc.sh @@ -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" diff --git a/dist/jni/decoder_subprocess.cpp b/dist/jni/decoder_subprocess.cpp new file mode 100644 index 0000000..320df61 --- /dev/null +++ b/dist/jni/decoder_subprocess.cpp @@ -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 +// 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 +#include +#include +#include +#include +#include + +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 \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 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 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; +} diff --git a/dist/jni/tts_engine.cpp b/dist/jni/tts_engine.cpp index 6cb3cbe..bd569c2 100644 --- a/dist/jni/tts_engine.cpp +++ b/dist/jni/tts_engine.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include 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);