diff --git a/CMakeLists.txt b/CMakeLists.txt index 97f1d79..d529e08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,16 @@ option(VOICEDETECT_GGML_METAL "Forward GGML_METAL" OFF) option(VOICEDETECT_GGML_VULKAN "Forward GGML_VULKAN" OFF) option(VOICEDETECT_GGML_HIP "Forward GGML_HIP (ROCm)" OFF) +set(VOICEDETECT_TOP_LEVEL OFF) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(VOICEDETECT_TOP_LEVEL ON) +endif() +option(VOICEDETECT_EXTERNAL_DR_WAV "Use the embedding project's dr_wav implementation" OFF) + +# An embedding host owns its ggml configuration; everything up to the ggml +# add_subdirectory below only applies to a top-level build. +if(VOICEDETECT_TOP_LEVEL) + # Multi-ISA CPU backend with RUNTIME dispatch. ggml can build one CPU backend # per x86 micro-arch (sse42/sandybridge/haswell/skylakex/icelake/zen4/...) as # dlopen'd modules and pick the best one the host actually supports AT LOAD TIME. @@ -130,7 +140,11 @@ if(BASH_EXECUTABLE AND EXISTS "${CMAKE_SOURCE_DIR}/scripts/apply_ggml_patches.sh endif() endif() -add_subdirectory(third_party/ggml) +endif() # VOICEDETECT_TOP_LEVEL + +if(NOT TARGET ggml) + add_subdirectory(third_party/ggml) +endif() set(VOICEDETECT_SRC src/voicedetect.cpp @@ -159,9 +173,12 @@ if(VOICEDETECT_SHARED) else() add_library(voicedetect STATIC ${VOICEDETECT_SRC}) endif() -target_include_directories(voicedetect PUBLIC include PRIVATE src ${CMAKE_SOURCE_DIR}/third_party) +target_include_directories(voicedetect PUBLIC include PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/third_party) target_compile_definitions(voicedetect PUBLIC $<$:_USE_MATH_DEFINES>) target_link_libraries(voicedetect PUBLIC ggml) +if(VOICEDETECT_EXTERNAL_DR_WAV) + target_compile_definitions(voicedetect PRIVATE VOICEDETECT_EXTERNAL_DR_WAV) +endif() if(VOICEDETECT_GGML_CUDNN) target_compile_definitions(voicedetect PRIVATE VOICEDETECT_GGML_CUDNN) endif() @@ -218,10 +235,10 @@ if(VOICEDETECT_HAS_AVX2) endif() endif() -if(VOICEDETECT_BUILD_CLI) +if(VOICEDETECT_BUILD_CLI AND VOICEDETECT_TOP_LEVEL) add_subdirectory(examples/cli) endif() -if(VOICEDETECT_BUILD_TESTS) +if(VOICEDETECT_BUILD_TESTS AND VOICEDETECT_TOP_LEVEL) enable_testing() add_subdirectory(tests) endif() diff --git a/examples/cli/CMakeLists.txt b/examples/cli/CMakeLists.txt index 53b80a7..d4f26a9 100644 --- a/examples/cli/CMakeLists.txt +++ b/examples/cli/CMakeLists.txt @@ -1,3 +1,3 @@ add_executable(voicedetect-cli main.cpp) target_link_libraries(voicedetect-cli PRIVATE voicedetect) -target_include_directories(voicedetect-cli PRIVATE ${CMAKE_SOURCE_DIR}/src) +target_include_directories(voicedetect-cli PRIVATE ${PROJECT_SOURCE_DIR}/src) diff --git a/include/voicedetect_capi.h b/include/voicedetect_capi.h index 5ff715f..fb1dda8 100644 --- a/include/voicedetect_capi.h +++ b/include/voicedetect_capi.h @@ -88,6 +88,11 @@ int voicedetect_capi_verify_paths(voicedetect_ctx* ctx, const char* a, char* voicedetect_capi_analyze_path_json(voicedetect_ctx* ctx, const char* wav_path); +// Size of the L2-normalized embedding this model produces (192 to 512), 0 for a +// model with no speaker embedding (the age/gender/emotion analyze heads), -1 for +// a NULL ctx. Additive: no ABI version bump. +int voicedetect_capi_embedding_dim(voicedetect_ctx* ctx); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/audio_io.cpp b/src/audio_io.cpp index d3b1ab2..1abfb48 100644 --- a/src/audio_io.cpp +++ b/src/audio_io.cpp @@ -1,4 +1,6 @@ -#define DR_WAV_IMPLEMENTATION +#ifndef VOICEDETECT_EXTERNAL_DR_WAV +#define DR_WAV_IMPLEMENTATION // an embedding project may provide it instead +#endif #include "dr_wav.h" #include "audio_io.hpp" #include "common.hpp" diff --git a/src/voicedetect_capi.cpp b/src/voicedetect_capi.cpp index 1e0922e..092285b 100644 --- a/src/voicedetect_capi.cpp +++ b/src/voicedetect_capi.cpp @@ -62,6 +62,11 @@ extern "C" int voicedetect_capi_abi_version(void) { return VOICEDETECT_CAPI_ABI_VERSION; } +extern "C" int voicedetect_capi_embedding_dim(voicedetect_ctx* ctx) { + if (!ctx || !ctx->model) return -1; + return ctx->model->embedding_dim(); +} + extern "C" voicedetect_ctx* voicedetect_capi_load(const char* gguf_path) { if (!gguf_path) return nullptr; try { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dcc8603..6ae1792 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,12 +1,13 @@ function(vd_add_test name) add_executable(${name} ${name}.cpp) target_link_libraries(${name} PRIVATE voicedetect) - target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/third_party) + target_include_directories(${name} PRIVATE ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/third_party) add_test(NAME ${name} COMMAND ${name}) set_tests_properties(${name} PROPERTIES SKIP_RETURN_CODE 77) endfunction() vd_add_test(test_smoke) +vd_add_test(test_capi_dim) vd_add_test(test_fbank) vd_add_test(test_encoder) vd_add_test(test_embed) @@ -19,37 +20,37 @@ vd_add_test(test_cache_safety) # Manual benchmark target (not registered as a ctest; run directly). add_executable(bench_wespeaker bench_wespeaker.cpp) target_link_libraries(bench_wespeaker PRIVATE voicedetect) -target_include_directories(bench_wespeaker PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/third_party) +target_include_directories(bench_wespeaker PRIVATE ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/third_party) add_executable(bench_eres2net bench_eres2net.cpp) target_link_libraries(bench_eres2net PRIVATE voicedetect) -target_include_directories(bench_eres2net PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/third_party) +target_include_directories(bench_eres2net PRIVATE ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/third_party) # Model/baseline-dependent tests read fixtures/baselines via paths relative to # the project root, and skip (exit 77) when the baseline env var is unset. set_tests_properties(test_fbank test_encoder test_embed test_verify test_analyze test_age_gender test_cache_safety PROPERTIES LABELS "model" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) # Python converter check (skips with exit 77 when the venv/model are absent). -find_program(VOICEDETECT_PY NAMES python3 HINTS ${CMAKE_SOURCE_DIR}/.venv/bin) +find_program(VOICEDETECT_PY NAMES python3 HINTS ${PROJECT_SOURCE_DIR}/.venv/bin) if(VOICEDETECT_PY) - add_test(NAME check_convert COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_convert.py) + add_test(NAME check_convert COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_convert.py) set_tests_properties(check_convert PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") # Analyze (wav2vec2 emotion) converter check: a SEPARATE analyze GGUF, skips # independently of the speaker-encoder check when its HF model is absent. - add_test(NAME check_convert_analyze COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_convert.py --analyze) + add_test(NAME check_convert_analyze COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_convert.py --analyze) set_tests_properties(check_convert_analyze PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") - add_test(NAME check_baseline_analyze COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_baseline.py --analyze) + add_test(NAME check_baseline_analyze COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_baseline.py --analyze) set_tests_properties(check_baseline_analyze PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") # Age/gender (audeering wav2vec2-large-robust) converter + baseline checks: a # SEPARATE analyze GGUF, skips independently when its HF model is absent. - add_test(NAME check_convert_age_gender COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_convert.py --age-gender) + add_test(NAME check_convert_age_gender COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_convert.py --age-gender) set_tests_properties(check_convert_age_gender PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") - add_test(NAME check_baseline_age_gender COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_baseline.py --age-gender) + add_test(NAME check_baseline_age_gender COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_baseline.py --age-gender) set_tests_properties(check_baseline_age_gender PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") - add_test(NAME check_manifest COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_manifest.py) + add_test(NAME check_manifest COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_manifest.py) set_tests_properties(check_manifest PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") - add_test(NAME check_baseline COMMAND ${VOICEDETECT_PY} ${CMAKE_SOURCE_DIR}/tests/python/check_baseline.py) + add_test(NAME check_baseline COMMAND ${VOICEDETECT_PY} ${PROJECT_SOURCE_DIR}/tests/python/check_baseline.py) set_tests_properties(check_baseline PROPERTIES SKIP_RETURN_CODE 77 LABELS "model") endif() diff --git a/tests/test_capi_dim.cpp b/tests/test_capi_dim.cpp new file mode 100644 index 0000000..c717665 --- /dev/null +++ b/tests/test_capi_dim.cpp @@ -0,0 +1,32 @@ +// voicedetect_capi_embedding_dim: NULL ctx is -1; with VOICEDETECT_TEST_GGUF set, +// it matches the length embed_pcm returns. Skips (77) without a model. +#include "voicedetect_capi.h" + +#include +#include +#include + +int main() { + if (voicedetect_capi_embedding_dim(nullptr) != -1) { + std::fprintf(stderr, "FAIL: NULL ctx should give -1\n"); + return 1; + } + const char* path = std::getenv("VOICEDETECT_TEST_GGUF"); + if (!path) return 77; + voicedetect_ctx* ctx = voicedetect_capi_load(path); + if (!ctx) { std::fprintf(stderr, "FAIL: load %s\n", path); return 1; } + const int dim = voicedetect_capi_embedding_dim(ctx); + std::vector pcm(32000); + for (size_t i = 0; i < pcm.size(); ++i) pcm[i] = 0.1f * (float)((i * 37) % 200 - 100) / 100.0f; + float* v = nullptr; + int n = 0; + const int rc = voicedetect_capi_embed_pcm(ctx, pcm.data(), (int)pcm.size(), 16000, &v, &n); + voicedetect_capi_free_vec(v); + voicedetect_capi_free(ctx); + if (rc != 0 || dim <= 0 || dim != n) { + std::fprintf(stderr, "FAIL: rc=%d dim=%d embed len=%d\n", rc, dim, n); + return 1; + } + std::printf("embedding_dim OK (%d)\n", dim); + return 0; +}