Kazeia-engine/dist/jni/test_jni_tts.cpp

121 lines
5.4 KiB
C++

// Harness JNI sans VM Java : on dlopen libkazeia_tts.so, on récupère les Java_*
// symbols, et on les appelle avec un JNIEnv mock minimal (juste les fonctions que
// kazeia_tts_jni.cpp utilise : GetStringUTFChars, ReleaseStringUTFChars, NewIntArray,
// SetIntArrayRegion). jstring est juste un alias opaque pour const char*.
//
// Permet de valider le bridge JNI sans installer l'app — utile pour CI / régression.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h> // _exit
#include <dlfcn.h>
#include <jni.h>
#include <string>
namespace {
// Pour notre mock, jstring = const char* tel quel (alias).
const char * mock_GetStringUTFChars(JNIEnv*, jstring s, jboolean*) { return (const char*)s; }
void mock_ReleaseStringUTFChars(JNIEnv*, jstring, const char*) {}
// jintArray = pointeur vers une struct {int n; jint data[n]} qu'on alloue.
struct IntArr { int n; jint data[1]; };
jintArray mock_NewIntArray(JNIEnv*, jsize len) {
auto* a = (IntArr*)malloc(sizeof(int) + (size_t)len * sizeof(jint));
a->n = len;
return (jintArray)a;
}
void mock_SetIntArrayRegion(JNIEnv*, jintArray arr, jsize start, jsize len, const jint* buf) {
auto* a = (IntArr*)arr;
memcpy(a->data + start, buf, (size_t)len * sizeof(jint));
}
struct MockNativeInterface {
// Les offsets et le layout doivent matcher jni.h JNINativeInterface. Plutôt que
// de reproduire toute la struct, on alloue une zone large, et on pose les
// function pointers AUX BONS OFFSETS (cf jni.h: GetStringUTFChars = entry 169,
// ReleaseStringUTFChars = 170, NewIntArray = 187, SetIntArrayRegion = 207).
// Pour rester portable, on utilise directement l'API via le struct JNINativeInterface
// déclaré dans jni.h.
JNINativeInterface iface{};
JNINativeInterface * ptr;
MockNativeInterface() {
// On laisse tout à nullptr, on remplit juste les méthodes utilisées.
iface.GetStringUTFChars = mock_GetStringUTFChars;
iface.ReleaseStringUTFChars = mock_ReleaseStringUTFChars;
iface.NewIntArray = mock_NewIntArray;
iface.SetIntArrayRegion = mock_SetIntArrayRegion;
ptr = &iface;
}
};
} // namespace
int main(int argc, char** argv) {
if (argc < 6) {
fprintf(stderr,"usage: %s <libkazeia_tts.so> <talker_gguf> <vocab_gguf> <dump_dir> <out.wav>\n", argv[0]);
return 1;
}
const char* lib_path = argv[1];
const char* talker_gguf = argv[2];
const char* vocab_gguf = argv[3];
const char* dump_dir = argv[4];
const char* out_wav = argv[5];
void* dl = dlopen(lib_path, RTLD_NOW | RTLD_LOCAL);
if (!dl) { fprintf(stderr, "dlopen FAIL %s : %s\n", lib_path, dlerror()); return 2; }
using FnLoad = jlong (*)(JNIEnv*, jobject, jstring, jstring, jstring, jboolean, jint, jboolean);
using FnSynth = jintArray (*)(JNIEnv*, jobject, jlong, jstring, jstring, jint, jint,
jfloat, jint, jfloat, jfloat, jfloat, jint, jfloat, jfloat);
using FnFree = void (*)(JNIEnv*, jobject, jlong);
auto p_load = (FnLoad)dlsym(dl, "Java_com_kazeia_tts_TtsJni_nativeLoad");
auto p_synth = (FnSynth)dlsym(dl, "Java_com_kazeia_tts_TtsJni_nativeSynthesize");
auto p_free = (FnFree)dlsym(dl, "Java_com_kazeia_tts_TtsJni_nativeFree");
if (!p_load || !p_synth || !p_free) {
fprintf(stderr, "dlsym FAIL : load=%p synth=%p free=%p\n", (void*)p_load, (void*)p_synth, (void*)p_free);
return 3;
}
MockNativeInterface ifc;
JNIEnv env_buf;
env_buf.functions = ifc.ptr;
JNIEnv* env = &env_buf;
jobject self = nullptr; // pas utilisé dans nos handlers
jlong h = p_load(env, self,
(jstring)talker_gguf, (jstring)vocab_gguf, (jstring)dump_dir,
/*useHtp=*/JNI_FALSE, /*nThreads=*/6, /*cpUseCache=*/JNI_TRUE);
if (h == 0) { fprintf(stderr, "nativeLoad returned 0\n"); dlclose(dl); return 4; }
fprintf(stderr,"nativeLoad OK -> handle=%p\n", (void*)(uintptr_t)h);
// 2 appels : même phrase puis phrase différente.
auto run = [&](const char* text, const char* out_path) {
auto* arr = p_synth(env, self, h,
(jstring)text, (jstring)out_path,
/*maxSteps=*/256, /*seed=*/42,
/*cpTemp=*/0.9f, /*cpTopK=*/50, /*cpTopP=*/1.0f, /*cpRepPenalty=*/1.05f,
/*talkerTemp=*/0.9f, /*talkerTopK=*/50, /*talkerTopP=*/1.0f, /*talkerRepPenalty=*/1.05f);
IntArr* a = (IntArr*)arr;
fprintf(stderr,"[%s] err=%d frames=%d total=%dms (pf=%d talker=%d cp=%d dec=%d) -> %s\n",
text, a->data[0], a->data[1], a->data[2], a->data[3], a->data[4], a->data[5], a->data[6], out_path);
int err = a->data[0];
free(a);
return err;
};
std::string out1 = out_wav; out1 += ".1.wav";
std::string out2 = out_wav; out2 += ".2.wav";
int e1 = run("Bonjour je m'appelle Kazeia", out1.c_str());
int e2 = run("Bonsoir, comment tu te sens ce soir ?", out2.c_str());
p_free(env, self, h);
fprintf(stderr,"OK e1=%d e2=%d\n", e1, e2);
fflush(stderr); fflush(stdout);
// Pas de dlclose ni return : destructors statiques de libllama/ggml peuvent segfaulter
// après unmap (atexit/global dtors). En contexte app Android, la .so reste chargée
// pour toute la vie du process, donc ce path n'existe pas. _exit court-circuite.
_exit((e1 == 0 && e2 == 0) ? 0 : 5);
}