diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c77723..52c4bfc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,21 +15,31 @@ option(CED_GGML_METAL "Forward GGML_METAL" OFF) option(CED_GGML_VULKAN "Forward GGML_VULKAN" OFF) option(CED_GGML_HIP "Forward GGML_HIP (ROCm)" OFF) -set(GGML_CUDA ${CED_GGML_CUDA} CACHE BOOL "" FORCE) -set(GGML_METAL ${CED_GGML_METAL} CACHE BOOL "" FORCE) -set(GGML_VULKAN ${CED_GGML_VULKAN} CACHE BOOL "" FORCE) -set(GGML_HIP ${CED_GGML_HIP} CACHE BOOL "" FORCE) +set(CED_TOP_LEVEL OFF) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(CED_TOP_LEVEL ON) +endif() + +option(CED_EXTERNAL_DR_WAV "Use the embedding project's dr_wav implementation" OFF) + +if(CED_TOP_LEVEL) + set(GGML_CUDA ${CED_GGML_CUDA} CACHE BOOL "" FORCE) + set(GGML_METAL ${CED_GGML_METAL} CACHE BOOL "" FORCE) + set(GGML_VULKAN ${CED_GGML_VULKAN} CACHE BOOL "" FORCE) + set(GGML_HIP ${CED_GGML_HIP} CACHE BOOL "" FORCE) -# Free perf wins (per parakeet.cpp / rt-detr.cpp): -march=native + tinyBLAS. -if(NOT DEFINED GGML_NATIVE) - set(GGML_NATIVE ON CACHE BOOL "ggml: optimize for the current system" FORCE) + # Free perf wins (per parakeet.cpp / rt-detr.cpp): -march=native + tinyBLAS. + if(NOT DEFINED GGML_NATIVE) + set(GGML_NATIVE ON CACHE BOOL "ggml: optimize for the current system" FORCE) + endif() + if(NOT DEFINED GGML_LLAMAFILE) + set(GGML_LLAMAFILE ON CACHE BOOL "ggml: use LLAMAFILE (tinyBLAS SGEMM)" FORCE) + endif() endif() -if(NOT DEFINED GGML_LLAMAFILE) - set(GGML_LLAMAFILE ON CACHE BOOL "ggml: use LLAMAFILE (tinyBLAS SGEMM)" FORCE) +if(NOT TARGET ggml) + add_subdirectory(third_party/ggml) endif() -add_subdirectory(third_party/ggml) - set(CED_SRC src/model_loader.cpp src/ced_runner.cpp @@ -45,7 +55,10 @@ if(CED_SHARED) else() add_library(ced STATIC ${CED_SRC}) endif() -target_include_directories(ced PUBLIC include PRIVATE src ${CMAKE_SOURCE_DIR}/third_party) +target_include_directories(ced PUBLIC include PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/third_party) +if(CED_EXTERNAL_DR_WAV) + target_compile_definitions(ced PRIVATE CED_EXTERNAL_DR_WAV) +endif() target_compile_definitions(ced PUBLIC $<$:_USE_MATH_DEFINES>) target_link_libraries(ced PUBLIC ggml) diff --git a/README.md b/README.md index 454f869..83e185f 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,12 @@ cmake --build build-shared -j | `CED_GGML_METAL` | OFF | Forward GGML_METAL to the submodule | | `CED_GGML_VULKAN` | OFF | Forward GGML_VULKAN to the submodule | | `CED_GGML_HIP` | OFF | Forward GGML_HIP (ROCm) to the submodule | +| `CED_EXTERNAL_DR_WAV` | OFF | Do not compile dr_wav; the embedding project provides it | To build for a GPU backend, forward its flag, e.g. `cmake -B build -DCED_GGML_METAL=ON`. +When ced.cpp is added with `add_subdirectory` by a project that already has a `ggml` target, it reuses that ggml. + At runtime a GPU build picks the first GPU it finds and falls back to CPU. Set `CED_DEVICE` to choose: `CED_DEVICE=cpu` forces the CPU, and a device name such as `CUDA0`, `Vulkan0` or `MTL0` selects that device (`ced-cli info` prints the one in use). The weights are uploaded to the device once at load. If the device has no kernel for an op, that graph runs through ggml's scheduler with a CPU fallback. --- @@ -158,7 +161,7 @@ if (json) { printf("%s\n", json); ced_capi_free_string(json); } ced_capi_free(ctx); ``` -The per-PCM entry points take an arbitrary mono window, so a realtime consumer can call them on a sliding buffer for live recognition. There is also a struct-array variant (`ced_capi_classify_pcm`) and a WAV-path variant (`ced_capi_classify_path_json`). See `include/ced_capi.h` for the full API. +The per-PCM entry points take an arbitrary mono window, so a realtime consumer can call them on a sliding buffer for live recognition. There is also a struct-array variant (`ced_capi_classify_pcm`), a WAV-path variant (`ced_capi_classify_path_json`), and `ced_capi_classify_pcm_probs`, which writes every class score in class-index order (no sorting, no allocation) for callers that want the raw distribution. See `include/ced_capi.h` for the full API. --- diff --git a/examples/cli/CMakeLists.txt b/examples/cli/CMakeLists.txt index e635df7..e522bb6 100644 --- a/examples/cli/CMakeLists.txt +++ b/examples/cli/CMakeLists.txt @@ -1,5 +1,11 @@ add_executable(ced-cli main.cpp) +if(CED_EXTERNAL_DR_WAV) + # libced.a was built without a dr_wav implementation (the embedding host + # supplies one for its own binary), but ced-cli is a separate executable + # that still needs the real symbols. + target_sources(ced-cli PRIVATE dr_wav_impl.cpp) +endif() target_link_libraries(ced-cli PRIVATE ced) target_include_directories(ced-cli PRIVATE - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/third_party) + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/third_party) diff --git a/examples/cli/dr_wav_impl.cpp b/examples/cli/dr_wav_impl.cpp new file mode 100644 index 0000000..c112740 --- /dev/null +++ b/examples/cli/dr_wav_impl.cpp @@ -0,0 +1,8 @@ +// ced-cli is its own executable, never the embedding host's binary, so it +// needs a real dr_wav implementation even when the `ced` library is built +// with CED_EXTERNAL_DR_WAV (which only promises that *the host's* binary +// supplies one). Compiled into ced-cli only when that option is set; the +// non-embedded build already gets the implementation from src/audio_io.cpp +// via libced.a. +#define DR_WAV_IMPLEMENTATION +#include "dr_wav.h" diff --git a/include/ced_capi.h b/include/ced_capi.h index 36b7c23..1d3cdb4 100644 --- a/include/ced_capi.h +++ b/include/ced_capi.h @@ -66,6 +66,13 @@ typedef struct { int ced_capi_classify_pcm(ced_ctx* ctx, const float* samples, int n_samples, int sample_rate, ced_tag* out, int max_tags); +// All class scores for mono float PCM, in class-index order (no sorting, no +// allocation). Writes min(n_out, ced_capi_num_classes(ctx)) floats into `out` +// and returns that count, or -1 on error (see ced_capi_last_error). Resamples +// like ced_capi_classify_pcm when `sample_rate` differs from the model rate. +int ced_capi_classify_pcm_probs(ced_ctx* ctx, const float* samples, int n_samples, + int sample_rate, float* out, int n_out); + // Free a string returned by a *_json function. Safe on NULL. void ced_capi_free_string(char* s); diff --git a/src/audio_io.cpp b/src/audio_io.cpp index 8decdcb..4f105c7 100644 --- a/src/audio_io.cpp +++ b/src/audio_io.cpp @@ -1,4 +1,6 @@ -#define DR_WAV_IMPLEMENTATION +#ifndef CED_EXTERNAL_DR_WAV +#define DR_WAV_IMPLEMENTATION // an embedding project may provide it instead +#endif #include "dr_wav.h" #include "audio_io.hpp" diff --git a/src/ced_capi.cpp b/src/ced_capi.cpp index d1a7ea5..0ce6c8d 100644 --- a/src/ced_capi.cpp +++ b/src/ced_capi.cpp @@ -175,6 +175,17 @@ int ced_capi_classify_pcm(ced_ctx* ctx, const float* samples, int n_samples, int return n; } +int ced_capi_classify_pcm_probs(ced_ctx* ctx, const float* samples, int n_samples, + int sample_rate, float* out, int n_out) { + auto* c = reinterpret_cast(ctx); + if (!c || !samples || !out || n_out <= 0) return -1; + std::vector probs; + if (!do_classify(c, samples, n_samples, sample_rate, probs)) return -1; + const int n = std::min(n_out, (int)probs.size()); + std::memcpy(out, probs.data(), (size_t)n * sizeof(float)); + return n; +} + void ced_capi_free_string(char* s) { std::free(s); } } // extern "C" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8cf000b..ba9e679 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,13 +1,13 @@ -set(CED_MODEL_GGUF "${CMAKE_SOURCE_DIR}/models/ced-base-f32.gguf") -set(CED_BASELINE_GGUF "${CMAKE_SOURCE_DIR}/tests/fixtures/ced-base.baseline.gguf") +set(CED_MODEL_GGUF "${PROJECT_SOURCE_DIR}/models/ced-base-f32.gguf") +set(CED_BASELINE_GGUF "${PROJECT_SOURCE_DIR}/tests/fixtures/ced-base.baseline.gguf") foreach(t blocks frontend quant capi) add_executable(test_${t} test_${t}.cpp) target_link_libraries(test_${t} PRIVATE ced) target_include_directories(test_${t} PRIVATE - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR}/third_party/ggml/include) + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/third_party/ggml/include) endforeach() add_test(NAME capi COMMAND test_capi "${CED_MODEL_GGUF}" "${CED_BASELINE_GGUF}") @@ -20,9 +20,9 @@ add_test(NAME frontend COMMAND test_frontend "${CED_MODEL_GGUF}" "${CED_BASELINE # python scripts/gen_ced_baseline.py --model mispeech/ced-base \ # --out tests/fixtures/ced-base-short.baseline.gguf --n-samples 64000 add_test(NAME frontend_short COMMAND test_frontend "${CED_MODEL_GGUF}" - "${CMAKE_SOURCE_DIR}/tests/fixtures/ced-base-short.baseline.gguf") + "${PROJECT_SOURCE_DIR}/tests/fixtures/ced-base-short.baseline.gguf") # End-to-end quantization parity (probs close + top-5 labels unchanged). -add_test(NAME e2e_f32 COMMAND test_quant "${CMAKE_SOURCE_DIR}/models/ced-base-f32.gguf" "${CED_BASELINE_GGUF}" 1e-3) -add_test(NAME e2e_f16 COMMAND test_quant "${CMAKE_SOURCE_DIR}/models/ced-base-f16.gguf" "${CED_BASELINE_GGUF}" 5e-3) -add_test(NAME e2e_q8 COMMAND test_quant "${CMAKE_SOURCE_DIR}/models/ced-base-q8_0.gguf" "${CED_BASELINE_GGUF}" 3e-2) +add_test(NAME e2e_f32 COMMAND test_quant "${PROJECT_SOURCE_DIR}/models/ced-base-f32.gguf" "${CED_BASELINE_GGUF}" 1e-3) +add_test(NAME e2e_f16 COMMAND test_quant "${PROJECT_SOURCE_DIR}/models/ced-base-f16.gguf" "${CED_BASELINE_GGUF}" 5e-3) +add_test(NAME e2e_q8 COMMAND test_quant "${PROJECT_SOURCE_DIR}/models/ced-base-q8_0.gguf" "${CED_BASELINE_GGUF}" 3e-2) diff --git a/tests/test_capi.cpp b/tests/test_capi.cpp index f835b5b..1a15a47 100644 --- a/tests/test_capi.cpp +++ b/tests/test_capi.cpp @@ -54,6 +54,30 @@ int main(int argc, char** argv) { ced_capi_free_string(json); } + // all-scores path: same numbers as the sorted top-k path, in index order + { + const int nc = ced_capi_num_classes(ctx); + std::vector all(nc, -1.0f); + int w = ced_capi_classify_pcm_probs(ctx, wav.data(), (int)wav.size(), 16000, + all.data(), nc); + ok &= (w == nc); + std::vector sorted(nc); + int ns = ced_capi_classify_pcm(ctx, wav.data(), (int)wav.size(), 16000, + sorted.data(), nc); + ok &= (ns == nc); + for (int i = 0; i < ns; ++i) ok &= (all[sorted[i].index] == sorted[i].score); + // short buffer: only the first n_out classes + float three[3]; + ok &= (ced_capi_classify_pcm_probs(ctx, wav.data(), (int)wav.size(), 16000, + three, 3) == 3); + ok &= (three[0] == all[0] && three[2] == all[2]); + // errors + ok &= (ced_capi_classify_pcm_probs(ctx, nullptr, 10, 16000, three, 3) == -1); + ok &= (ced_capi_classify_pcm_probs(ctx, wav.data(), (int)wav.size(), 16000, + nullptr, 3) == -1); + std::fprintf(stderr, "classify_pcm_probs: %s\n", ok ? "ok" : "FAIL"); + } + ced_capi_free(ctx); std::fprintf(stderr, "%s\n", ok ? "PASS" : "FAIL"); return ok ? 0 : 1;