diff --git a/scripts/ui-assets.cmake b/scripts/ui-assets.cmake index 89f22b14a..78a0f4c84 100644 --- a/scripts/ui-assets.cmake +++ b/scripts/ui-assets.cmake @@ -16,6 +16,7 @@ set(HF_VERSION "" CACHE STRING "Version to download (empty = resolve from set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)") set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)") set(LLAMA_UI_EMBED "" CACHE STRING "Path to llama-ui-embed helper") +set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth") set(DIST_DIR "${UI_BINARY_DIR}/dist") set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist") @@ -225,6 +226,33 @@ function(hf_download version out_var out_resolved) endfunction() function(emit_files dist_dir) + # If gzip is requested, compress every asset into a parallel _gzip/ tree + # the structure stays the same; for ex: /abc/def --> /_gzip/abc/def + # embed.cpp will check for _gzip and will pick it up + if(LLAMA_UI_GZIP AND EXISTS "${dist_dir}/index.html") + find_program(GZIP_EXECUTABLE gzip) + if(NOT GZIP_EXECUTABLE) + message(WARNING "UI: LLAMA_UI_GZIP requested but gzip not found, embedding uncompressed") + else() + set(gzip_dir "${dist_dir}/_gzip") + file(REMOVE_RECURSE "${gzip_dir}") + file(GLOB_RECURSE all_files RELATIVE "${dist_dir}" "${dist_dir}/*") + foreach(f ${all_files}) + get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY) + file(MAKE_DIRECTORY "${dst_dir}") + execute_process( + COMMAND "${GZIP_EXECUTABLE}" -c "${dist_dir}/${f}" + OUTPUT_FILE "${gzip_dir}/${f}" + RESULT_VARIABLE gz_rc + ) + if(NOT gz_rc EQUAL 0) + message(FATAL_ERROR "UI: gzip failed for ${f}") + endif() + endforeach() + message(STATUS "UI: gzip compression applied (${gzip_dir})") + endif() + endif() + set(args "${UI_CPP}" "${UI_H}") if(EXISTS "${dist_dir}/index.html") list(APPEND args "${dist_dir}") diff --git a/tools/server/server-http.cpp b/tools/server/server-http.cpp index e79f0cbab..3c58fcfec 100644 --- a/tools/server/server-http.cpp +++ b/tools/server/server-http.cpp @@ -319,8 +319,26 @@ bool server_http_context::init(const common_params & params) { } } else { #if defined(LLAMA_UI_HAS_ASSETS) + static auto handle_gzip_header = [](const httplib::Request & req, httplib::Response & res) { + if (!llama_ui_use_gzip()) { + // no gzip build, skip + return true; + } + if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) { + res.status = 415; // unsupported media type + res.set_content("Error: gzip is not supported by this browser", "text/plain"); + return false; + } else { + res.set_header("Content-Encoding", "gzip"); + } + return true; + }; + auto serve_asset_cached = [](const std::string & name, bool isolation) { return [name, isolation](const httplib::Request & req, httplib::Response & res) { + if (!handle_gzip_header(req, res)) { + return true; // returns error message + } const llama_ui_asset * a = llama_ui_find_asset(name); if (!a) { res.status = 404; return false; } res.set_header("ETag", a->etag); @@ -340,7 +358,10 @@ bool server_http_context::init(const common_params & params) { }; auto serve_asset_nocache = [](const std::string & name) { - return [name](const httplib::Request & /*req*/, httplib::Response & res) { + return [name](const httplib::Request & req, httplib::Response & res) { + if (!handle_gzip_header(req, res)) { + return true; // returns error message + } const llama_ui_asset * a = llama_ui_find_asset(name); if (!a) { res.status = 404; diff --git a/tools/ui/CMakeLists.txt b/tools/ui/CMakeLists.txt index abea13228..74bca417e 100644 --- a/tools/ui/CMakeLists.txt +++ b/tools/ui/CMakeLists.txt @@ -1,6 +1,7 @@ set(TARGET llama-ui) set(LLAMA_UI_HF_BUCKET "ggml-org/llama-ui" CACHE STRING "Hugging Face bucket name for prebuilt UI assets") +set(LLAMA_UI_GZIP ON CACHE BOOL "Apply gzip compress to assets to save bandwidth") # Backward compat: forward old var to new one if(DEFINED LLAMA_BUILD_WEBUI) @@ -83,6 +84,7 @@ add_custom_target(llama-ui-assets ALL "-DHF_ENABLED=${LLAMA_USE_PREBUILT_UI}" "-DBUILD_UI=${LLAMA_BUILD_UI}" "-DLLAMA_UI_EMBED=${LLAMA_UI_EMBED_EXE}" + "-DLLAMA_UI_GZIP=${LLAMA_UI_GZIP}" -P "${PROJECT_SOURCE_DIR}/scripts/ui-assets.cmake" COMMENT "Provisioning UI assets" VERBATIM diff --git a/tools/ui/embed.cpp b/tools/ui/embed.cpp index 2ca022208..d4c222f64 100644 --- a/tools/ui/embed.cpp +++ b/tools/ui/embed.cpp @@ -1,7 +1,7 @@ // llama-ui-embed: generate ui.cpp / ui.h that embed UI assets as C arrays. // // Usage: -// llama-ui-embed [] +// llama-ui-embed // // Recursively embeds every regular file under . // Asset names are relative paths from (e.g. "_app/immutable/bundle.HASH.js"). @@ -145,12 +145,15 @@ int main(int argc, char ** argv) { return 1; } - const std::string out_cpp = argv[1]; - const std::string out_h = argv[2]; - const std::string in_dir = argv[3]; + const std::string out_cpp = argv[1]; + const std::string out_h = argv[2]; + const std::string asset_dir = argv[3]; + + const bool use_gzip = std::filesystem::exists(asset_dir + "/_gzip"); + const std::string in_dir = use_gzip ? (asset_dir + "/_gzip") : asset_dir; std::vector assets; - if (argc == 4) { + if (!in_dir.empty()) { const std::filesystem::path dir = in_dir; std::error_code ec; @@ -238,7 +241,8 @@ int main(int argc, char ** argv) { " std::string etag;\n" " std::string type;\n" "};\n\n" - "const llama_ui_asset * llama_ui_find_asset(const std::string & name);\n"; + "const llama_ui_asset * llama_ui_find_asset(const std::string & name);\n" + "bool llama_ui_use_gzip();\n"; h += fmt("const std::array & llama_ui_get_assets();\n", n_assets); std::string cpp; @@ -294,6 +298,7 @@ int main(int argc, char ** argv) { " return empty;\n" "}\n"; } + cpp += fmt("bool llama_ui_use_gzip() { return %s; }\n", use_gzip ? "true" : "false"); bool ok = true; ok = write_if_different(out_h, h) && ok;