Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,25 @@ endif()

add_subdirectory(ggml EXCLUDE_FROM_ALL)
add_subdirectory(thirdparty/CLI11 EXCLUDE_FROM_ALL)
set(HTTPLIB_REQUIRE_OPENSSL ON CACHE BOOL "HF downloads require HTTPS" FORCE)
add_subdirectory(thirdparty/cpp-httplib EXCLUDE_FROM_ALL)
add_subdirectory(thirdparty/json EXCLUDE_FROM_ALL)

include(cmake/icu.cmake)
add_subdirectory(src)

add_library(clap-hf STATIC src/hf.cpp)
set_target_properties(clap-hf PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_EXTENSIONS OFF)
target_compile_features(clap-hf PUBLIC cxx_std_17)
target_include_directories(clap-hf PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(clap-hf PRIVATE httplib::httplib nlohmann_json::nlohmann_json)


# The CLI target differs from the core library target, while its executable
# keeps the existing "clap" name and command-line behavior.
add_executable(clap-cli src/clap.cpp src/embedding.cpp src/server.cpp src/quantize.cpp)
target_compile_features(clap-cli PRIVATE cxx_std_17)
target_link_libraries(clap-cli PRIVATE clap CLI11::CLI11 httplib::httplib nlohmann_json::nlohmann_json)
target_link_libraries(clap-cli PRIVATE clap clap-hf CLI11::CLI11 httplib::httplib nlohmann_json::nlohmann_json)
set_target_properties(clap-cli PROPERTIES OUTPUT_NAME clap CXX_EXTENSIONS OFF)

if(MSVC)
Expand All @@ -58,6 +66,15 @@ if(BUILD_TESTING)
add_test(NAME clap_conversion_imports
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/conversion.py")
set_tests_properties(clap_conversion_imports PROPERTIES TIMEOUT 30)
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
add_executable(clap-hf-tls-fixture tests/hf_tls_fixture.cpp)
target_compile_features(clap-hf-tls-fixture PRIVATE cxx_std_17)
target_link_libraries(clap-hf-tls-fixture PRIVATE OpenSSL::Crypto)
add_test(NAME clap_hf_download
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/hf_download.py"
"$<TARGET_FILE:clap-cli>" "$<TARGET_FILE:clap-hf-tls-fixture>")
set_tests_properties(clap_hf_download PROPERTIES TIMEOUT 60)

find_program(FFMPEG_EXECUTABLE NAMES ffmpeg REQUIRED)
set(CLAP_AUDIO_FIXTURE_DIR "${CMAKE_CURRENT_BINARY_DIR}/tests/audio")
set(CLAP_AUDIO_FIXTURES
Expand Down
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require a converted GGUF model; inference runs locally without Python or downloa

- CMake 3.20 or newer
- A C++17 compiler
- OpenSSL 3 development headers/libraries and a trusted CA certificate store (HTTPS downloads; e.g. `libssl-dev` and `ca-certificates` on Debian/Ubuntu)
- ICU source submodule (included by recursive checkout; no system ICU installation required)
- Python 3 (only for model conversion and tests; install Python dependencies with `python -m pip install -r requirements.txt`)
- FFmpeg with libmp3lame (only when building tests, to generate WAV/MP3/FLAC fixtures)
Expand Down Expand Up @@ -174,6 +175,84 @@ This adds a release build, not HIP model dispatch: native model linear layers
remain CPU/optional Vulkan. The embedding command and server use that same
native runtime. No ROCm inference or GPU tests are run.

## Download GGUF models from Hugging Face

Download a **converted, unsplit GGUF** with the native C++ HTTP client. No Python,
`huggingface-cli`, Git or git-lfs is needed at runtime. Replace `owner/repo` below
with a repository containing a CLAP/CLaMP3/MERT2 GGUF compatible with this loader:

```sh
./build/clap download -hf owner/repo
./build/clap download -hf owner/repo:Q8_0
./build/clap download -hf owner/repo -hff subdirectory/model.gguf
./build/clap embedding -hf owner/repo:Q4_K_M --text 'A jazz quartet.'
# Existing local model inference is unchanged:
./build/clap embedding --model model.gguf --text 'A jazz quartet.'
```

`-hf`, `-hff` and `-hft` are aliases for `--hf-repo`, `--hf-file` and
`--hf-token`. The default selection prefers a filename containing `Q4_K_M`, then
`Q8_0`, then the first GGUF in lexical filename order. `:quant` filters filenames
case-insensitively; `-hff` selects an exact repository-relative filename and takes
precedence over the quant selector. Split `-00001-of-00002.gguf` models are rejected
before downloading: the clap loader does not support split GGUFs. Downloading does
**not** convert safetensors/PyTorch weights or make unrelated architectures usable.
For repositories without a GGUF, use the conversion instructions below first.

The command resolves `main` to an immutable commit and enumerates the recursive
repository tree (including paginated results). Successful downloads print only
the absolute local snapshot path to stdout; download status and errors go to
stderr. `clap embedding` resolves the same cache path before loading the model and
keeps its embedding JSON on stdout. `--model` and `-hf` are mutually exclusive.

### Cache, authentication and network settings

Cache root precedence (first nonempty value):

1. `CLAP_CACHE`
2. `HF_HUB_CACHE`
3. `HUGGINGFACE_HUB_CACHE` (legacy alias)
4. `$HF_HOME/hub`
5. `$XDG_CACHE_HOME/huggingface/hub`, or `$HOME/.cache/huggingface/hub`
(falling back to `%USERPROFILE%/.cache/huggingface/hub` when `HOME` is unset)

The layout is Hugging Face-compatible: `models--owner--repo/blobs/<oid>`,
`snapshots/<commit>/<filename>` (relative symlinks to blobs, or hard links when
symlink creation is unavailable), and `refs/main`.
Completed files are reused. Online calls still refresh metadata; add `--offline`
to use only the locally recorded `main` snapshot without any network requests.
An offline cache miss is an error, not a fallback to the network.

```sh
CLAP_CACHE=/path/to/models ./build/clap download -hf owner/repo
./build/clap download -hf owner/repo --offline
./build/clap embedding -hf owner/repo --offline --text 'A jazz quartet.'
# Set HF_TOKEN in your environment for private/gated repositories.
# Alternatively: ./build/clap download -hf owner/repo -hft "$HF_TOKEN"
```

`-hft` overrides `HF_TOKEN`; prefer the environment to avoid putting credentials
in shell history or process arguments. Tokens are sent as bearer credentials only
to the configured origin, never to a different redirect host. `HF_ENDPOINT`
(default `https://huggingface.co`) accepts an HTTP(S) origin for mirrors or local
testing. Use HTTPS for real credentials. TLS certificate verification remains
enabled; OpenSSL 3 support is required at configure time. Set `SSL_CERT_FILE` to a
PEM CA bundle when using a private certificate authority; this overrides the default
trust store without disabling certificate or hostname verification.

Downloads stream into `.downloadInProgress` files, resume with validated HTTP
Range responses, and publish completed blobs/snapshots atomically after size
validation (and SHA256 verification for LFS blobs). Network/server failures retry
at most three times per request. Directory traversal and unsafe cache symlinks
are refused, including Windows drive/device/alternate-stream filenames on every
platform. The cache must be writable and support symlinks or hard links; do not
share a writable cache with untrusted users. A per-repository `.clap-download.lock`
rejects concurrent writers; after a crash, remove that stale lock directory only
when no download is running. Partial files remain available for the next resume.
An interrupted snapshot publication is recovered under that lock only when its
`.downloadInProgress` link points to the exact expected blob; unrelated temporary
files or links are refused and left untouched.

## Native model layout

The C++ organization follows `llama.cpp/src/models`: shared runtime services are
Expand Down
37 changes: 34 additions & 3 deletions src/clap.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "embedding.h"
#include "hf.h"
#include "quantize.h"
#include "server.h"

Expand All @@ -8,6 +9,7 @@
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

int main(int argc, char **argv) {
CLI::App app{"clap.cpp"};
Expand All @@ -28,8 +30,17 @@ int main(int argc, char **argv) {
EmbeddingOptions options;
std::string file;
auto *embedding = app.add_subcommand("embedding", "Generate native text or audio embeddings");
embedding->add_option("-m,--model", options.model_path, "Converted CLAP/CLaMP3/MERT/MERT2/MuQ/MuLan GGUF")
->required()->check(CLI::ExistingFile);
auto *model_option = embedding->add_option("-m,--model", options.model_path, "Converted CLAP/CLaMP3/MERT/MERT2/MuQ/MuLan GGUF")
->check(CLI::ExistingFile);
clap::HfOptions embedding_hf;
auto *hf_option = embedding->add_option("--hf-repo", embedding_hf.repo, "owner/repo[:quant] (-hf)");
hf_option->excludes(model_option);
embedding->add_option("--hf-file", embedding_hf.file, "Exact GGUF filename (-hff)")->needs(hf_option);
embedding->add_option("--hf-token", embedding_hf.token, "Bearer token (-hft; defaults to HF_TOKEN)")->needs(hf_option);
embedding->add_flag("--offline", embedding_hf.offline, "Use only cached HF model")->needs(hf_option);
auto *model = embedding->add_option_group("Model");
model->add_options(model_option, hf_option);
model->require_option(1, 1);
embedding->add_option("-t,--threads", options.threads, "ggml CPU threads (0: automatic)")
->check(CLI::NonNegativeNumber)->capture_default_str();
auto *input = embedding->add_option_group("Input", "Choose one text prompt, text file, or audio file");
Expand Down Expand Up @@ -62,7 +73,26 @@ int main(int argc, char **argv) {
quantize->add_option("INPUT.gguf", quantize_input, "F32/F16 CLAP/CLaMP3/MERT/MERT2/MuQ/MuLan model")->required();
quantize->add_option("OUTPUT.gguf", quantize_output, "Output model (must not exist)")->required();

CLI11_PARSE(app, argc, argv);
clap::HfOptions hf;
auto *download = app.add_subcommand("download", "Download a Hugging Face GGUF model");
download->add_option("--hf-repo", hf.repo, "owner/repo[:quant] (-hf)")->required();
download->add_option("--hf-file", hf.file, "Exact GGUF filename (-hff)");
download->add_option("--hf-token", hf.token, "Bearer token (-hft; defaults to HF_TOKEN)");
download->add_flag("--offline", hf.offline, "Use only an existing cached snapshot");
auto arguments = clap::hf_arguments(argc, argv);
std::vector<char *> pointers;
for (auto &arg : arguments) pointers.push_back(arg.data());
CLI11_PARSE(app, argc, pointers.data());
if (*download) {
try {
std::cout << clap::hf_download(hf) << '\n';
if (!std::cout) throw std::runtime_error("Cannot write downloaded path");
return 0;
} catch (const std::exception &e) {
std::cerr << e.what() << '\n';
return 1;
}
}

if (*quantize) {
return run_quantize(quantize_input, quantize_output, quantize_type);
Expand Down Expand Up @@ -94,6 +124,7 @@ int main(int argc, char **argv) {
}
if (!*audio_option && !options.single_text && options.text.empty())
throw std::runtime_error("Input must not be empty");
if (*hf_option) options.model_path = clap::hf_download(embedding_hf);
return run_embedding(options);
}
if (*serve) {
Expand Down
Loading
Loading