From e0eae4d8fee1502aac9f31914576f9902a5b8be4 Mon Sep 17 00:00:00 2001 From: nomoreshow <45514669+nomoreshow@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:34:55 +0300 Subject: [PATCH 1/9] Ship a managed Vulkan whisper-server for Linux x64 --- .github/workflows/whisper-vulkan.yml | 189 ++++++++++++++++++ README.md | 5 +- README.tr.md | 3 + dikte/ggml.py | 60 ++++-- packaging/whisper-vulkan/Dockerfile.build | 44 ++++ .../whisper-vulkan/Dockerfile.runtime-cpu | 6 + .../whisper-vulkan/Dockerfile.runtime-vulkan | 7 + packaging/whisper-vulkan/build-package.sh | 128 ++++++++++++ .../licenses/cpp-httplib-MIT.txt | 21 ++ .../licenses/nlohmann-json-MIT.txt | 21 ++ .../whisper-vulkan/lunarg-signing-key-pub.asc | 31 +++ packaging/whisper-vulkan/make-sbom.py | 116 +++++++++++ packaging/whisper-vulkan/smoke-runtime.sh | 64 ++++++ packaging/whisper-vulkan/validate-package.sh | 145 ++++++++++++++ tests/test_ggml.py | 112 +++++++++++ tests/test_packaging.py | 180 +++++++++++++++++ 16 files changed, 1117 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/whisper-vulkan.yml create mode 100644 packaging/whisper-vulkan/Dockerfile.build create mode 100644 packaging/whisper-vulkan/Dockerfile.runtime-cpu create mode 100644 packaging/whisper-vulkan/Dockerfile.runtime-vulkan create mode 100755 packaging/whisper-vulkan/build-package.sh create mode 100644 packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt create mode 100644 packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt create mode 100644 packaging/whisper-vulkan/lunarg-signing-key-pub.asc create mode 100755 packaging/whisper-vulkan/make-sbom.py create mode 100755 packaging/whisper-vulkan/smoke-runtime.sh create mode 100755 packaging/whisper-vulkan/validate-package.sh create mode 100644 tests/test_packaging.py diff --git a/.github/workflows/whisper-vulkan.yml b/.github/workflows/whisper-vulkan.yml new file mode 100644 index 0000000..e3bdf05 --- /dev/null +++ b/.github/workflows/whisper-vulkan.yml @@ -0,0 +1,189 @@ +name: whisper.cpp Vulkan bundle + +on: + pull_request: + paths: + - packaging/whisper-vulkan/** + - .github/workflows/whisper-vulkan.yml + - dikte/ggml.py + - tests/test_ggml.py + - tests/test_packaging.py + - README.md + - README.tr.md + workflow_dispatch: + inputs: + whisper_version: + description: Upstream whisper.cpp version (without v) + required: true + default: "1.9.3" + type: string + whisper_commit: + description: Peeled commit SHA for that reviewed upstream tag + required: true + default: "371b5a7561823ab2bb32142d2751e35e7534727b" + type: string + publish: + description: Publish a Dikte dependency release + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: whisper-vulkan-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + WHISPER_VERSION: ${{ inputs.whisper_version || '1.9.3' }} + WHISPER_COMMIT: ${{ inputs.whisper_commit || '371b5a7561823ab2bb32142d2751e35e7534727b' }} + MANAGED_WHISPER_SHA256: c25ca76504144da488eb74441390a7b9aa7ce547e5f2f391cbd831253c9b54d8 + +jobs: + build: + runs-on: ubuntu-22.04 + timeout-minutes: 45 + steps: + - name: Check out Dikte + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Check out pinned whisper.cpp source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: ggml-org/whisper.cpp + ref: ${{ env.WHISPER_COMMIT }} + path: vendor/whisper.cpp + fetch-depth: 0 + persist-credentials: false + + - name: Validate source coordinates + shell: bash + run: | + set -euo pipefail + [[ "$WHISPER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "$WHISPER_COMMIT" =~ ^[0-9a-f]{40}$ ]] + + - name: Verify source version and commit + shell: bash + run: | + set -euo pipefail + test "$(git -C vendor/whisper.cpp rev-parse HEAD)" = "$WHISPER_COMMIT" + git -C vendor/whisper.cpp fetch --depth=1 origin \ + "refs/tags/v$WHISPER_VERSION:refs/tags/v$WHISPER_VERSION" + test "$(git -C vendor/whisper.cpp rev-list -n1 "v$WHISPER_VERSION")" = "$WHISPER_COMMIT" + echo "SOURCE_DATE_EPOCH=$(git -C vendor/whisper.cpp show -s --format=%ct HEAD)" >> "$GITHUB_ENV" + + - name: Build pinned build environment + run: docker build --pull=false -f packaging/whisper-vulkan/Dockerfile.build -t dikte-whisper-builder packaging/whisper-vulkan + + - name: Build deterministic archive + run: | + docker run --rm \ + -e WHISPER_VERSION -e WHISPER_COMMIT -e SOURCE_DATE_EPOCH \ + -v "$PWD/vendor/whisper.cpp:/src:ro" \ + -v "$PWD/packaging/whisper-vulkan:/packaging:ro" \ + -v "$PWD/work:/work" \ + dikte-whisper-builder \ + bash /packaging/build-package.sh + mkdir -p dist + cp work/out/whisper-bin-ubuntu-vulkan-x64.* dist/ + + - name: Verify reviewed archive digest + shell: bash + run: | + read -r actual _ < dist/whisper-bin-ubuntu-vulkan-x64.tar.gz.sha256 + test "$actual" = "$MANAGED_WHISPER_SHA256" + + - name: Validate archive and ELF contract + run: OUT_DIR=dist packaging/whisper-vulkan/validate-package.sh + + - name: Schema-validate CycloneDX 1.6 SBOM + run: | + docker run --rm \ + -v "$PWD/dist/whisper-bin-ubuntu-vulkan-x64.cdx.json:/sbom.json:ro" \ + cyclonedx/cyclonedx-cli@sha256:252c2e26f468c25fea1e63ecde1bc3198ad6e9dbb57f5ed3236bddcb2281b3a7 \ + validate --input-file /sbom.json --input-format json \ + --input-version v1_6 --fail-on-errors + + - name: CPU fallback smoke test (no Vulkan loader) + run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh cpu + + - name: Vulkan plugin-load smoke test (Mesa llvmpipe) + run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh vulkan + + - name: Upload reviewed outputs + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: whisper-bin-ubuntu-vulkan-x64 + path: dist/* + if-no-files-found: error + retention-days: 14 + + publish: + if: >- + github.event_name == 'workflow_dispatch' && inputs.publish && + github.ref == 'refs/heads/master' + needs: build + runs-on: ubuntu-22.04 + environment: dependency-release + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + steps: + - name: Download the exact tested outputs + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: whisper-bin-ubuntu-vulkan-x64 + path: dist + + - name: Verify digest sidecar + run: (cd dist && sha256sum --check whisper-bin-ubuntu-vulkan-x64.tar.gz.sha256) + + - name: Attest build provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + with: + subject-path: dist/whisper-bin-ubuntu-vulkan-x64.tar.gz + + - name: Attest SBOM to archive + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + with: + subject-path: dist/whisper-bin-ubuntu-vulkan-x64.tar.gz + sbom-path: dist/whisper-bin-ubuntu-vulkan-x64.cdx.json + + - name: Publish dependency release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: whisper.cpp-v${{ inputs.whisper_version }} + RELEASE_TITLE: whisper.cpp v${{ inputs.whisper_version }} Vulkan bundle + RELEASE_NOTES: >- + Pinned source: ggml-org/whisper.cpp@${{ inputs.whisper_commit }}. + Verify with: gh attestation verify + whisper-bin-ubuntu-vulkan-x64.tar.gz + --repo ${{ github.repository }} + run: | + set -euo pipefail + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "refusing to replace existing release $RELEASE_TAG" >&2 + exit 1 + fi + if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" >/dev/null 2>&1; then + echo "refusing to replace existing tag $RELEASE_TAG" >&2 + exit 1 + fi + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f ref="refs/tags/$RELEASE_TAG" \ + -f sha="$GITHUB_SHA" >/dev/null + test "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" \ + --jq .object.sha)" = "$GITHUB_SHA" + gh release create "$RELEASE_TAG" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --prerelease \ + --latest=false \ + --title "$RELEASE_TITLE" \ + --notes "$RELEASE_NOTES" diff --git a/README.md b/README.md index 8853b8f..601311e 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,10 @@ running. the program and the model, verifies the sha256 and refuses a download published without one, then keeps a server alive while you dictate. The graphics card is reached through CUDA, ROCm or Vulkan where the build allows. No key, no - account, nothing leaving the machine. + account, nothing leaving the machine. On x86_64 Linux, that same Download + button tries the reviewed Vulkan bundle when a loader is present, and falls + back to upstream's CPU build if it is unavailable. The bundle also carries CPU + backends for systems where a Vulkan device cannot start. - **Silence never reaches the API.** Handed near-silence, a transcription model invents a sentence instead of returning nothing ("Thanks for watching", or in Turkish "Altyazı M.K."). A recording is dropped when nothing rose 10 dB above diff --git a/README.tr.md b/README.tr.md index 2e6a461..0fc592f 100644 --- a/README.tr.md +++ b/README.tr.md @@ -160,6 +160,9 @@ olmasını ister. checksum'suz yayınlanmış bir indirmeyi reddeder, sen dikte ettikçe sunucuyu ayakta tutar. Derleme destekliyorsa ekran kartına CUDA, ROCm ya da Vulkan üzerinden ulaşılır. Anahtar yok, hesap yok, makineden çıkan bir şey yok. + x86_64 Linux'ta aynı İndir düğmesi, Vulkan yükleyicisi varsa incelenmiş Vulkan + paketini dener; paket kullanılamıyorsa upstream'in işlemci derlemesine döner. + Vulkan aygıtı başlatılamadığında kullanılacak işlemci arka uçları da pakettedir. - **Sessizlik API'ye gitmez.** Sessize yakın bir ses verildiğinde model boş dize döndürmez, bir cümle uydurur ("Altyazı M.K.", "Thanks for watching"). *O kaydın kendi* gürültü tabanının 10 dB üstüne en az 0,3 saniye çıkan bir şey diff --git a/dikte/ggml.py b/dikte/ggml.py index c2f8da8..792a72d 100644 --- a/dikte/ggml.py +++ b/dikte/ggml.py @@ -76,6 +76,13 @@ Program = collections.namedtuple("Program", "name repo binary health") WHISPER = Program("whisper", "ggml-org/whisper.cpp", "whisper-server", "") LLAMA = Program("llama", "ggml-org/llama.cpp", "llama-server", "/health") +DIKTE_REPO = "yusufipk/dikte" +MANAGED_WHISPER_RELEASE = "whisper.cpp-v1.9.3" +MANAGED_WHISPER_VERSION = "v1.9.3" +MANAGED_WHISPER_VULKAN = "whisper-bin-ubuntu-vulkan-x64.tar.gz" +MANAGED_WHISPER_SHA256 = ( + "c25ca76504144da488eb74441390a7b9aa7ce547e5f2f391cbd831253c9b54d8" +) # Where the models are listed. Neither list is written into Dikte: a catalogue # in the source means a release of Dikte for every model somebody else @@ -346,8 +353,11 @@ def _extract(archive, into): with tarfile.open(archive, "r:gz") as tar: try: tar.extractall(into, filter="data") - except TypeError: # Python without the extraction filters - tar.extractall(into) + except TypeError as exc: # Python 3.11.0-3 lack extraction filters + raise LocalError(t( + "Could not safely unpack {name} with this Python version", + name=os.path.basename(str(archive)), + )) from exc except (tarfile.TarError, zipfile.BadZipFile, OSError) as exc: raise LocalError(t("Could not unpack {name}: {error}", name=os.path.basename(str(archive)), error=exc)) from exc @@ -370,20 +380,42 @@ def install_program(program, tag="", on_progress=None, should_stop=None, refresh=False): """Fetch and unpack a release. The path to the binary, or "" when stopped. - `tag` is empty for whatever the project released last, which is the point: - a version pinned in Dikte's source would mean a release of Dikte every time - whisper.cpp has one. + Ordinary builds follow the program's newest release. The Linux Vulkan build + comes from Dikte's pinned dependency release instead. """ - try: - tag, assets = hub.release(program.repo, tag or "latest", refresh=refresh) - except hub.HubError as exc: - raise LocalError(str(exc)) from exc - + repo = program.repo + release_tag = tag or "latest" + managed = (not tag and program is WHISPER and sys.platform == "linux" + and platform.machine().lower() in ("x86_64", "amd64") + and _has_vulkan()) item = None - for ending in _wanted_assets(program): - item = next((a for a in assets if a.name.endswith(ending)), None) + if managed: + repo = DIKTE_REPO + release_tag = MANAGED_WHISPER_RELEASE + try: + tag, assets = hub.release(repo, release_tag, refresh=refresh) + except hub.HubError: + # Older Dikte releases have no managed server. The upstream CPU + # build remains the usable answer there and during API failures. + assets = [] + item = next((a for a in assets + if a.name.endswith(MANAGED_WHISPER_VULKAN) + and a.sha256 == MANAGED_WHISPER_SHA256), None) if item: - break + tag = MANAGED_WHISPER_VERSION + + if item is None: + repo = program.repo + wanted = _wanted_assets(program) + try: + tag, assets = hub.release(repo, "latest" if managed else release_tag, + refresh=refresh) + except hub.HubError as exc: + raise LocalError(str(exc)) from exc + for ending in wanted: + item = next((a for a in assets if a.name.endswith(ending)), None) + if item: + break if item is None: # Nothing to download and nothing to install for you: whisper.cpp # publishes no macOS binary, and Homebrew's whisper-cpp is configured @@ -398,7 +430,7 @@ def install_program(program, tag="", on_progress=None, should_stop=None, "or transcribe in the cloud. See the README." )) raise LocalError(t("{repo} {tag} has no build for this machine.", - repo=program.repo, tag=tag)) + repo=repo, tag=tag)) into = BIN_DIR / program.name / tag fresh = into.with_name(tag + ".new") diff --git a/packaging/whisper-vulkan/Dockerfile.build b/packaging/whisper-vulkan/Dockerfile.build new file mode 100644 index 0000000..fae985b --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.build @@ -0,0 +1,44 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc + +ARG DEBIAN_FRONTEND=noninteractive +ARG CMAKE_VERSION=3.31.6 +ARG CMAKE_SHA256=5a1133ff103c71eb5120e2cc3de922733e7d8a26a98ae716397e8676adb367bf + +COPY lunarg-signing-key-pub.asc /tmp/lunarg.asc + +RUN set -eux; \ + test "$(sha256sum /tmp/lunarg.asc | cut -d' ' -f1)" = aa1c3c29673140e77f0d6a9aaeed5d9b5621e305ead51c59fae4458bbb4df92b; \ + apt-get update; \ + apt-get install --no-install-recommends -y \ + build-essential=12.9ubuntu3 \ + ca-certificates \ + curl \ + file \ + git \ + gnupg \ + ninja-build=1.10.1-1 \ + patchelf=0.14.3-1 \ + python3 \ + xz-utils; \ + install -d -m 0755 /usr/share/keyrings; \ + gpg --dearmor -o /usr/share/keyrings/lunarg.gpg /tmp/lunarg.asc; \ + printf '%s\n' 'deb [signed-by=/usr/share/keyrings/lunarg.gpg] https://packages.lunarg.com/vulkan jammy main' \ + > /etc/apt/sources.list.d/lunarg-vulkan.list; \ + apt-get update; \ + apt-get install --no-install-recommends -y \ + libvulkan-dev=1.4.313.0~rc1-1lunarg22.04-1 \ + vulkan-headers=1.4.313.0~rc1-1lunarg22.04-1 \ + shaderc=2025.2~rc1-1lunarg22.04-1 \ + spirv-headers=1.6.1+1.4.313.0~rc1-1lunarg22.04-1; \ + curl --fail --location --retry 3 \ + "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.tar.gz" \ + -o /tmp/cmake.tar.gz; \ + test "$(sha256sum /tmp/cmake.tar.gz | cut -d' ' -f1)" = "$CMAKE_SHA256"; \ + tar -xzf /tmp/cmake.tar.gz --strip-components=1 -C /usr/local; \ + rm -rf /var/lib/apt/lists/* /tmp/cmake.tar.gz /tmp/lunarg.asc; \ + cmake --version; \ + glslc --version; \ + test -f /usr/include/vulkan/vulkan.h; \ + test -f /usr/share/cmake/SPIRV-Headers/SPIRV-HeadersConfig.cmake + +WORKDIR /work diff --git a/packaging/whisper-vulkan/Dockerfile.runtime-cpu b/packaging/whisper-vulkan/Dockerfile.runtime-cpu new file mode 100644 index 0000000..89ac704 --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.runtime-cpu @@ -0,0 +1,6 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install --no-install-recommends -y ca-certificates curl libstdc++6 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /bundle diff --git a/packaging/whisper-vulkan/Dockerfile.runtime-vulkan b/packaging/whisper-vulkan/Dockerfile.runtime-vulkan new file mode 100644 index 0000000..3a16288 --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.runtime-vulkan @@ -0,0 +1,7 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + ca-certificates curl libstdc++6 libvulkan1 mesa-vulkan-drivers vulkan-tools \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /bundle diff --git a/packaging/whisper-vulkan/build-package.sh b/packaging/whisper-vulkan/build-package.sh new file mode 100755 index 0000000..f4e2215 --- /dev/null +++ b/packaging/whisper-vulkan/build-package.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -euo pipefail +shopt -s nullglob + +: "${SOURCE_DIR:=/src}" +: "${OUT_DIR:=/work/out}" +: "${WHISPER_VERSION:=1.9.3}" +: "${WHISPER_COMMIT:=371b5a7561823ab2bb32142d2751e35e7534727b}" +: "${SOURCE_DATE_EPOCH:=1787219223}" + +export SOURCE_DATE_EPOCH TZ=UTC LC_ALL=C LANG=C +asset=whisper-bin-ubuntu-vulkan-x64 +build=/work/build +source_copy=/work/source +root="$OUT_DIR/root/$asset" + +rm -rf "$build" "$source_copy" "$OUT_DIR" +mkdir -p "$build" "$root/LICENSES" +# Upstream configures bindings/javascript/package.json in the source directory. +# Build a private copy so the checked-out, verified source remains untouched. +cp -a "$SOURCE_DIR" "$source_copy" +chmod -R u+w "$source_copy" +git config --global --add safe.directory "$source_copy" + +cmake -S "$source_copy" -B "$build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_BUILD_RPATH='$ORIGIN' \ + -DCMAKE_INSTALL_RPATH='$ORIGIN' \ + -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ + -DCMAKE_C_FLAGS="-ffile-prefix-map=$source_copy=. -fdebug-prefix-map=$source_copy=. -fmacro-prefix-map=$source_copy=." \ + -DCMAKE_CXX_FLAGS="-ffile-prefix-map=$source_copy=. -fdebug-prefix-map=$source_copy=. -fmacro-prefix-map=$source_copy=." \ + -DBUILD_SHARED_LIBS=ON \ + -DGGML_BACKEND_DL=ON \ + -DGGML_CPU_ALL_VARIANTS=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_CCACHE=OFF \ + -DGGML_OPENMP=OFF \ + -DGGML_VULKAN=ON \ + -DWHISPER_BUILD_EXAMPLES=ON \ + -DWHISPER_BUILD_SERVER=ON \ + -DWHISPER_BUILD_TESTS=OFF \ + -DWHISPER_BUILD_IS_DEV=OFF \ + -DWHISPER_CURL=OFF \ + -DWHISPER_SDL2=OFF \ + -DWHISPER_COMMON_FFMPEG=OFF \ + -DWHISPER_BUILD_COMMIT="$WHISPER_COMMIT" \ + -DWHISPER_BUILD_NUMBER=0 +cmake --build "$build" --target whisper-server --parallel "$(nproc)" + +# Package an allowlist, not everything examples/ happens to build in the future. +cp -a "$build/bin/whisper-server" "$root/" +cp -a "$build/bin"/libwhisper.so* "$root/" +cp -a "$build/bin"/libggml.so* "$root/" +cp -a "$build/bin"/libggml-base.so* "$root/" +cp -a "$build/bin"/libggml-cpu*.so* "$root/" +cp -a "$build/bin"/libggml-vulkan.so* "$root/" + +# Strip real ELF files only; preserve the SONAME symlink chains. +while IFS= read -r -d '' file; do + if file "$file" | grep -q ELF; then + strip --strip-unneeded "$file" + patchelf --set-rpath '$ORIGIN' "$file" + fi +done < <(find "$root" -type f -print0) + +cp "$SOURCE_DIR/LICENSE" "$root/LICENSES/whisper.cpp-MIT.txt" +cp /packaging/licenses/cpp-httplib-MIT.txt "$root/LICENSES/" +cp /packaging/licenses/nlohmann-json-MIT.txt "$root/LICENSES/" + +cat > "$root/BUILD-INFO.json" < "$root/$asset.cdx.json" + +( + cd "$root" + find . -type f ! -name SHA256SUMS -print0 \ + | sort -z \ + | xargs -0 sha256sum +) > "$root/SHA256SUMS" + +mkdir -p "$OUT_DIR" +tar --sort=name --owner=0 --group=0 --numeric-owner \ + --mtime="@$SOURCE_DATE_EPOCH" \ + --pax-option=delete=atime,delete=ctime \ + -C "$OUT_DIR/root" -cf - "$asset" \ + | gzip -n -9 > "$OUT_DIR/$asset.tar.gz" +( + cd "$OUT_DIR" + sha256sum "$asset.tar.gz" > "$asset.tar.gz.sha256" +) +cp "$root/$asset.cdx.json" "$OUT_DIR/$asset.cdx.json" diff --git a/packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt b/packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt new file mode 100644 index 0000000..47c418e --- /dev/null +++ b/packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 yhirose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt b/packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt new file mode 100644 index 0000000..70c6af6 --- /dev/null +++ b/packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/whisper-vulkan/lunarg-signing-key-pub.asc b/packaging/whisper-vulkan/lunarg-signing-key-pub.asc new file mode 100644 index 0000000..59b5d72 --- /dev/null +++ b/packaging/whisper-vulkan/lunarg-signing-key-pub.asc @@ -0,0 +1,31 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFuOrjYBCADT5MjShtbeSsWHADqVP7PZIp+m/wWkSUA7/FX/qrixhQE9DFyt +XtKSbBdwh+Jg5nsttCUiePtdrrRD1tcyowG256Tus3vOysZzvpfjWA4gcVmTjJXn +gwezKsPZLQi0wvjwQD8ByxnM1i2eiJC4xcMjT21uZkwDfgLTzVO4InWlVyZDB/da +PLJl4r1MqnsI603RKalMQmZzs43YUDssdeOiGOpXvb1Rj0XcsOOqnAEvIwyUWGku +1Hr+b6C9Nj6wksD7TCB10IdOeuwBqFgrVDzicG4fijwnpzA+UUfncIhKYdI/oIvj +mcAPobWzcBkM3uc+Yf/CxlBahzu6jv7AFdT1ABEBAAG0VEx1bmFyRyBTaWduaW5n +IEtleSAoS2V5IHVzZWQgYnkgTHVuYXJHIHRvIHNpZ24gcGFja2FnZXMpIDxsaW51 +eC1wYWNrYWdlc0BsdW5hcmcuY29tPokBTgQTAQoAOBYhBAP11iGjcQ+pWpPYm6qE +UggOOD9+BQJbjq42AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEKqEUggO +OD9+ECgH/Ro6LVB08FifApBS235v0Af3dsJlZGE0miKu2hR12qAvWackE6//E5GN +5xKSNpgLzV6kyylBntQDhcFzW3hLt/AsMLOXvuxYNFcLes2y10DrqVekNeJiR95V +KiTPI2jP8m4eFpcSnY0riHk2MmstN1icehQhYrWFyUtt3VxSsRWiRDeNUfCHC6YP +MjOXonmTWfH7T+UA2IqLFrt9dAsYGiCtMKVgzaZaZwm727c0aqy0e43nsWqjWxmE +EsEA1RvzjKKyzyixwpnzIyQ8dqL8sH0G3E2OYTlS7A8//yfgykRQVHwg2TsTBKfG +LlTmKj7RCT6GqISo+rbYYo/hZ6l2hH25AQ0EW46uNgEIANZfPWerTPzmvswWqp0P +iQvW+0qTBxZH3gQlwq5s6ahpY1pIebfrL/SAYJUGyjJVcjkG+HBXRGyRxtWFDE+D ++WEuziBfKd3aBUXb5DnvWdCiXeyQnFfwUVYNXhU5PlpAB5M409a30p9gGOrYy3Ah +g4VHhpM9wzGUAOzTwQ4WaC2WkR84sZYyqdKoo6C3m4IR4KHMYXF9nRlPSNEckL9U +MZe6I2uvor9FOPIfIOAI8lN+gbj/anf3lfy0ZYPyUtl3EWveGpWAPvdw3LMKg5QN +B8bR9TkPk0YZyQQcWkmN7gLUg0Vba+PYHH9DRlG8w1rH4TKxXJV3wmHo2aZRF1kc +30kAEQEAAYkBNgQYAQoAIBYhBAP11iGjcQ+pWpPYm6qEUggOOD9+BQJbjq42AhsM +AAoJEKqEUggOOD9+MEUH/2pm2QOttjd7DmEaS4LGvaTlEif0xtymRAh3axGuqQhl +KCZbw0jwsQlo/DwMRZwZHYCj1A/5H8mEg9qNGjF35GEpQTFSQI6Mt7F2DK69J86w +61v8tjxs4eO201ndhy+DRwDwG8vryFldx3f0nEdlE7IusgiUdvkcJPc8rX7p0MJJ +istTREAq8bRnvWYJzd4k3tgwHglEDxyjBRwLtqZyQ19XZb3V/aVKygqvZbwdJyXO +RHAZxK81p9Gp/8VkogJHLx6+3V8UlDepJg9/8MUCBQ9wWkdF0Pfqzgu7xtIHSxvW +62EF4nxqVuC946OIeITgXpd4F+iTFVII8w0P+nyCzac= +=nXAe +-----END PGP PUBLIC KEY BLOCK----- diff --git a/packaging/whisper-vulkan/make-sbom.py b/packaging/whisper-vulkan/make-sbom.py new file mode 100755 index 0000000..1dcf99c --- /dev/null +++ b/packaging/whisper-vulkan/make-sbom.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +import datetime +import hashlib +import json +import os +import uuid +from pathlib import Path + +root = Path(os.environ["ROOT"]) +version = os.environ["VERSION"] +commit = os.environ["COMMIT"] +epoch = int(os.environ["EPOCH"]) +asset = "whisper-bin-ubuntu-vulkan-x64" +sbom_path = root / f"{asset}.cdx.json" + +def digest(path): + h = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + h.update(block) + return h.hexdigest() + +files = [] +for path in sorted(root.rglob("*")): + if path != sbom_path and path.is_file() and not path.is_symlink(): + rel = path.relative_to(root).as_posix() + files.append({ + "type": "file", + "bom-ref": f"file:{rel}", + "name": rel, + "hashes": [{"alg": "SHA-256", "content": digest(path)}], + }) + +ts = datetime.datetime.fromtimestamp( + epoch, datetime.timezone.utc, +).isoformat().replace("+00:00", "Z") +root_ref = f"pkg:github/ggml-org/whisper.cpp@{version}?commit={commit}" +ggml_ref = "pkg:github/ggml-org/ggml@0.20.2" +httplib_ref = "pkg:github/yhirose/cpp-httplib@0.20.0" +json_ref = "pkg:github/nlohmann/json@3.11.2" + +sbom = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": f"urn:uuid:{uuid.uuid5(uuid.NAMESPACE_URL, root_ref)}", + "version": 1, + "metadata": { + "timestamp": ts, + "tools": {"components": [ + {"type": "application", "name": "make-sbom.py", "version": "1"}, + {"type": "application", "name": "CMake", "version": "3.31.6"}, + {"type": "application", "name": "glslc", "version": "2025.2"}, + ]}, + "component": { + "type": "application", + "bom-ref": root_ref, + "group": "ggml-org", + "name": "whisper-server", + "version": version, + "purl": root_ref, + "licenses": [{"expression": "MIT"}], + "externalReferences": [{ + "type": "vcs", + "url": f"https://github.com/ggml-org/whisper.cpp/tree/{commit}", + }], + "properties": [ + {"name": "dikte:asset-name", "value": f"{asset}.tar.gz"}, + {"name": "dikte:source-commit", "value": commit}, + {"name": "dikte:runtime:glibc-minimum", "value": "2.34"}, + {"name": "dikte:runtime:glibcxx-minimum", "value": "3.4.30"}, + {"name": "dikte:runtime:vulkan-loader", "value": "optional; libvulkan.so.1"}, + ], + }, + }, + "components": [ + { + "type": "library", + "bom-ref": ggml_ref, + "group": "ggml-org", + "name": "ggml", + "version": "0.20.2", + "purl": ggml_ref, + "licenses": [{"expression": "MIT"}], + "properties": [{ + "name": "dikte:source", + "value": "vendored by the pinned whisper.cpp commit", + }], + }, + { + "type": "library", + "bom-ref": httplib_ref, + "group": "yhirose", + "name": "cpp-httplib", + "version": "0.20.0", + "purl": httplib_ref, + "licenses": [{"expression": "MIT"}], + }, + { + "type": "library", + "bom-ref": json_ref, + "group": "nlohmann", + "name": "json", + "version": "3.11.2", + "purl": json_ref, + "licenses": [{"expression": "MIT"}], + }, + *files, + ], + "dependencies": [{ + "ref": root_ref, + "dependsOn": [ggml_ref, httplib_ref, json_ref] + + [item["bom-ref"] for item in files], + }], +} +json.dump(sbom, fp=os.sys.stdout, indent=2, sort_keys=True) +print() diff --git a/packaging/whisper-vulkan/smoke-runtime.sh b/packaging/whisper-vulkan/smoke-runtime.sh new file mode 100755 index 0000000..22c0626 --- /dev/null +++ b/packaging/whisper-vulkan/smoke-runtime.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode=${1:?usage: smoke-runtime.sh cpu|vulkan} +: "${OUT_DIR:=work/out}" +: "${FIXTURE_SOURCE:=vendor/whisper.cpp}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_DIR="$(realpath "$OUT_DIR")" +FIXTURE_SOURCE="$(realpath "$FIXTURE_SOURCE")" +asset=whisper-bin-ubuntu-vulkan-x64 +case "$mode" in + cpu) dockerfile=Dockerfile.runtime-cpu; image=dikte-whisper-runtime-cpu:spike ;; + vulkan) dockerfile=Dockerfile.runtime-vulkan; image=dikte-whisper-runtime-vulkan:spike ;; + *) echo "unknown mode: $mode" >&2; exit 2 ;; +esac + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +tar -xzf "$OUT_DIR/$asset.tar.gz" -C "$tmp" +docker build --pull=false -f "$SCRIPT_DIR/$dockerfile" -t "$image" "$SCRIPT_DIR" + +args=("/bundle/$asset/whisper-server" -m /fixtures/model.bin + --host 127.0.0.1 --port 8080 + --inference-path /v1/audio/transcriptions -l auto -sns -nlp) +env_args=() +if [[ "$mode" == cpu ]]; then + args+=(-ng) +else + env_args=(-e LIBGL_ALWAYS_SOFTWARE=1 + -e VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json) +fi + +docker run --rm --name "dikte-whisper-$mode-smoke" \ + -e SMOKE_MODE="$mode" \ + "${env_args[@]}" \ + -v "$tmp/$asset:/bundle/$asset:ro" \ + -v "$FIXTURE_SOURCE/models/for-tests-ggml-base.en.bin:/fixtures/model.bin:ro" \ + -v "$FIXTURE_SOURCE/samples/jfk.wav:/fixtures/jfk.wav:ro" \ + "$image" bash -ec ' + if [ "$SMOKE_MODE" = cpu ] && ldconfig -p | grep -q libvulkan.so.1; then + echo "CPU smoke image unexpectedly has a Vulkan loader" >&2 + exit 1 + fi + "$@" >/tmp/server.log 2>&1 & + pid=$! + trap "kill $pid 2>/dev/null || true" EXIT + for _ in $(seq 1 120); do + kill -0 "$pid" 2>/dev/null || { cat /tmp/server.log; exit 1; } + if curl --silent --show-error --fail --max-time 180 \ + -F file=@/fixtures/jfk.wav -F response_format=json \ + http://127.0.0.1:8080/v1/audio/transcriptions >/tmp/response.json; then + grep -q "\"text\"" /tmp/response.json + if [ "$SMOKE_MODE" = vulkan ]; then + grep -q "loaded Vulkan backend" /tmp/server.log + fi + cat /tmp/response.json + cat /tmp/server.log + exit 0 + fi + sleep 1 + done + cat /tmp/server.log + exit 1 + ' bash "${args[@]}" diff --git a/packaging/whisper-vulkan/validate-package.sh b/packaging/whisper-vulkan/validate-package.sh new file mode 100755 index 0000000..d21a083 --- /dev/null +++ b/packaging/whisper-vulkan/validate-package.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${OUT_DIR:=work/out}" +: "${SOURCE_DIR:=whisper.cpp}" +asset=whisper-bin-ubuntu-vulkan-x64 +archive="$OUT_DIR/$asset.tar.gz" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +test -s "$archive" +(cd "$OUT_DIR" && sha256sum --check "$asset.tar.gz.sha256") +ARCHIVE="$archive" ASSET="$asset" python3 - <<'PY' +import os +import posixpath +import tarfile + +archive = os.environ["ARCHIVE"] +asset = os.environ["ASSET"] + + +def under_root(name): + normalized = posixpath.normpath(name) + return (not posixpath.isabs(normalized) + and normalized != ".." + and not normalized.startswith("../") + and normalized.split("/", 1)[0] == asset) + + +with tarfile.open(archive, "r:gz") as bundle: + for member in bundle: + if not under_root(member.name): + raise SystemExit(f"unsafe archive member: {member.name}") + if member.isdev() or member.isfifo(): + raise SystemExit(f"special archive member: {member.name}") + if not (member.isdir() or member.isfile() + or member.issym() or member.islnk()): + raise SystemExit(f"unsupported archive member: {member.name}") + if member.issym(): + target = posixpath.join(posixpath.dirname(member.name), + member.linkname) + if not under_root(target): + raise SystemExit(f"unsafe symlink: {member.name}") + if member.islnk() and not under_root(member.linkname): + raise SystemExit(f"unsafe hardlink: {member.name}") +PY +tar -xzf "$archive" -C "$tmp" +root="$tmp/$asset" + +test -x "$root/whisper-server" +test -f "$root/libwhisper.so" +test -f "$root/libggml.so" +test -f "$root/libggml-base.so" +test -f "$root/libggml-vulkan.so" +compgen -G "$root/libggml-cpu-*.so" >/dev/null +test -f "$root/LICENSES/whisper.cpp-MIT.txt" +test -f "$root/LICENSES/cpp-httplib-MIT.txt" +test -f "$root/LICENSES/nlohmann-json-MIT.txt" +(cd "$root" && sha256sum --check SHA256SUMS) + +# All shipped ELF objects must be relocatable and must not remember /work. +while IFS= read -r -d '' file; do + file "$file" | grep -q ELF || continue + dynamic=$(readelf -d "$file") + if ! grep -Fq 'Library runpath: [$ORIGIN]' <<<"$dynamic"; then + echo "runpath is not \$ORIGIN in $file" >&2 + exit 1 + fi + if grep -Eq '/(home|tmp|work)/' <<<"$dynamic"; then + echo "build path remains in $file" >&2 + exit 1 + fi +done < <(find "$root" -type f -print0) + +# Vulkan remains a plugin dependency. The executable must start without a loader. +if readelf -d "$root/whisper-server" | grep -q 'libvulkan.so'; then + echo "whisper-server links Vulkan instead of loading it as a plugin" >&2 + exit 1 +fi +readelf -d "$root/libggml-vulkan.so" | grep -q 'libvulkan.so.1' + +# Ubuntu 22.04 establishes the glibc ceiling promised by this artifact. +ROOT="$root" python3 - <<'PY' +import os, pathlib, re, subprocess +root = pathlib.Path(os.environ['ROOT']) +seen = {'GLIBC': set(), 'GLIBCXX': set(), 'CXXABI': set()} +external = { + 'libc.so.6', 'libgcc_s.so.1', 'libm.so.6', 'libstdc++.so.6', + 'libvulkan.so.1', 'ld-linux-x86-64.so.2', +} +for path in root.iterdir(): + if not path.is_file() or path.is_symlink(): + continue + header = subprocess.run(['readelf', '-h', path], text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL).stdout + if not header: + continue + if 'Machine: Advanced Micro Devices X86-64' not in header: + raise SystemExit(f'wrong ELF architecture: {path.name}') + dynamic = subprocess.run(['readelf', '-d', path], text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL).stdout + needed = re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic) + unexpected = [name for name in needed + if name not in external + and not re.fullmatch( + r'lib(?:whisper|ggml(?:-base)?)\.so\.\d+', name)] + if unexpected: + raise SystemExit( + f'unexpected DT_NEEDED in {path.name}: {unexpected}') + if path.name != 'libggml-vulkan.so' and 'libvulkan.so.1' in needed: + raise SystemExit(f'Vulkan is not plugin-only in {path.name}') + contents = path.read_bytes() + for marker in (b'/home/', b'/tmp/', b'/work/'): + if marker in contents: + raise SystemExit( + f'build path {marker!r} remains in {path.name}') + text = subprocess.run(['objdump', '-T', path], text=True, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout + for family in seen: + pattern = rf'{family}_([0-9]+(?:\.[0-9]+)+)' + seen[family].update(tuple(map(int, version.split('.'))) + for version in re.findall(pattern, text)) +assert seen['GLIBC'] and max(seen['GLIBC']) <= (2, 34), max(seen['GLIBC']) +assert seen['GLIBCXX'] and max(seen['GLIBCXX']) <= (3, 4, 30), max(seen['GLIBCXX']) +assert seen['CXXABI'] and max(seen['CXXABI']) <= (1, 3, 13), max(seen['CXXABI']) +for family, versions in seen.items(): + print(f'maximum {family} symbol:', '.'.join(map(str, max(versions)))) +PY + +python3 - "$root/$asset.cdx.json" <<'PY' +import json, sys +with open(sys.argv[1], encoding='utf-8') as stream: + doc = json.load(stream) +assert doc['bomFormat'] == 'CycloneDX' +assert doc['specVersion'] == '1.6' +assert doc['metadata']['component']['name'] == 'whisper-server' +assert len(doc['components']) >= 3 +print('SBOM components:', len(doc['components'])) +PY + +LD_LIBRARY_PATH='' "$root/whisper-server" --help >/dev/null 2>&1 + +echo "structure: PASS" diff --git a/tests/test_ggml.py b/tests/test_ggml.py index 44b9305..94a69b5 100644 --- a/tests/test_ggml.py +++ b/tests/test_ggml.py @@ -221,6 +221,7 @@ class InstallProgram(Local): def install(self, *names, archive=None): self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: False) blob = self.archive if archive is None else archive with serving(self.release(*names, archive=blob), blob) as calls: path = ggml.install_program(ggml.WHISPER) @@ -238,6 +239,105 @@ class InstallProgram(Local): "whisper-bin-ubuntu-x64.tar.gz") self.assertTrue(urls[1].endswith("whisper-bin-ubuntu-x64.tar.gz")) + def test_linux_x64_with_vulkan_takes_diktes_accelerated_build(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-vulkan-x64.tar.gz") + listing["tag_name"] = "whisper.cpp-v1.9.3" + managed_sha = hashlib.sha256(self.archive).hexdigest() + with mock.patch.object(ggml, "MANAGED_WHISPER_SHA256", managed_sha, + create=True): + with fake_urlopen(listing, body(self.archive)) as calls: + path = ggml.install_program(ggml.WHISPER) + urls = [call.full_url for call in calls] + self.assertIn( + "/repos/yusufipk/dikte/releases/tags/whisper.cpp-v1.9.3", + urls[0], + ) + self.assertTrue(urls[1].endswith( + "whisper-bin-ubuntu-vulkan-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + self.assertEqual("v1.9.3", ggml.installed_version(ggml.WHISPER)) + + def test_an_explicit_whisper_version_still_comes_from_upstream(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(listing, body(self.archive)) as calls: + ggml.install_program(ggml.WHISPER, tag="v1.9.1") + self.assertIn( + "/repos/ggml-org/whisper.cpp/releases/tags/v1.9.1", + calls[0].full_url, + ) + + def test_linux_arm64_keeps_using_the_upstream_cpu_build(self): + self.patch_attr(ggml, "_arch", lambda: "arm64") + self.patch_attr(ggml.platform, "machine", lambda: "aarch64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-arm64.tar.gz") + with fake_urlopen(listing, body(self.archive)) as calls: + ggml.install_program(ggml.WHISPER) + self.assertIn( + "/repos/ggml-org/whisper.cpp/releases/latest", + calls[0].full_url, + ) + + def test_linux_non_x86_does_not_try_the_managed_x64_build(self): + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-arm64.tar.gz") + with mock.patch("platform.machine", return_value="ppc64le"): + with fake_urlopen(listing, listing) as calls: + with self.assertRaises(ggml.LocalError): + ggml.install_program(ggml.WHISPER) + self.assertIn( + "/repos/ggml-org/whisper.cpp/releases/latest", + calls[0].full_url, + ) + + def test_a_missing_managed_build_falls_back_to_upstream_cpu(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + managed = self.release("Dikte-1.1.0-x86_64.AppImage") + managed["tag_name"] = "whisper.cpp-v1.9.3" + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(managed, upstream, body(self.archive)) as calls: + path = ggml.install_program(ggml.WHISPER) + urls = [call.full_url for call in calls] + self.assertIn( + "/repos/yusufipk/dikte/releases/tags/whisper.cpp-v1.9.3", + urls[0], + ) + self.assertIn("/repos/ggml-org/whisper.cpp/releases/latest", urls[1]) + self.assertTrue(urls[2].endswith("whisper-bin-ubuntu-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + + def test_a_managed_build_with_an_unreviewed_digest_falls_back(self): + self.patch_attr(ggml, "_has_vulkan", lambda: True) + managed = self.release("whisper-bin-ubuntu-vulkan-x64.tar.gz") + managed["assets"][0]["digest"] = "sha256:" + "0" * 64 + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(managed, upstream, body(self.archive)) as calls: + try: + path = ggml.install_program(ggml.WHISPER) + except ggml.LocalError as exc: + self.fail(f"unreviewed digest did not fall back: {exc}") + urls = [call.full_url for call in calls] + self.assertEqual(3, len(urls)) + self.assertTrue(urls[2].endswith("whisper-bin-ubuntu-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + + def test_an_unavailable_managed_release_falls_back_to_upstream_cpu(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(http_error(404), upstream, + body(self.archive)) as calls: + path = ggml.install_program(ggml.WHISPER) + self.assertEqual(3, len(calls)) + self.assertTrue(calls[2].full_url.endswith( + "whisper-bin-ubuntu-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + def test_a_release_with_nothing_for_this_machine_says_so(self): self.patch_attr(ggml, "_arch", lambda: "x64") with fake_urlopen(self.release("whisper-bin-Win32.zip")): @@ -399,6 +499,18 @@ class InstallProgram(Local): with self.assertRaises(ggml.LocalError): self.install("whisper-bin-ubuntu-x64.tar.gz", archive=buf.getvalue()) + def test_python_without_safe_tar_filters_refuses_the_archive(self): + archive = self.path("bundle.tar.gz") + archive.write_bytes(self.archive) + destination = self.path("unpacked") + destination.mkdir() + with mock.patch.object(tarfile.TarFile, "extractall", + side_effect=[TypeError("no filter"), None]) as extract: + with self.assertRaises(ggml.LocalError) as caught: + ggml._extract(archive, destination) + self.assertEqual(1, extract.call_count) + self.assertIn("safely", str(caught.exception)) + def test_everything_is_asked_for_over_tls(self): for url in (hub.GITHUB_API, hub.HF_API, hub.HF_FILES): with self.subTest(url=url): diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..2d63e6e --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,180 @@ +"""The release build that makes Linux Vulkan a one-click install.""" + +import hashlib +import io +import json +import os +import pathlib +import shutil +import subprocess +import sys +import tarfile +import tempfile +import unittest + +from dikte import ggml + + +ROOT = pathlib.Path(__file__).parents[1] +PACKAGING = ROOT / "packaging" / "whisper-vulkan" +WORKFLOW = ROOT / ".github" / "workflows" / "whisper-vulkan.yml" + + +class WhisperVulkanPackaging(unittest.TestCase): + @unittest.skipUnless(shutil.which("bash"), "bash is unavailable") + def test_the_release_scripts_parse_as_shell(self): + for name in ("build-package.sh", "validate-package.sh", + "smoke-runtime.sh"): + script = PACKAGING / name + checked = subprocess.run( + ["bash", "-n", script], capture_output=True, text=True, + ) + self.assertEqual("", checked.stderr) + self.assertEqual(0, checked.returncode) + + def test_the_workflow_builds_validates_smokes_and_publishes(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + for step in ("Build deterministic archive", + "Verify reviewed archive digest", + "Validate archive and ELF contract", + "CPU fallback smoke test (no Vulkan loader)", + "Vulkan plugin-load smoke test (Mesa llvmpipe)", + "Publish dependency release"): + self.assertIn(step, workflow) + self.assertNotRegex(workflow, r"uses: [^\n]+@v\d+(?:\s|$)") + + def test_publish_is_safe_for_dikte_and_limited_to_reviewed_master(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertGreaterEqual(workflow.count("persist-credentials: false"), 2) + self.assertIn("github.ref == 'refs/heads/master'", workflow) + self.assertIn("--prerelease", workflow) + self.assertIn("--latest=false", workflow) + self.assertIn("--verify-tag", workflow) + self.assertIn("refusing to replace existing tag", workflow) + self.assertIn("^[0-9]+\\.[0-9]+\\.[0-9]+$", workflow) + self.assertIn("^[0-9a-f]{40}$", workflow) + publish_script = workflow.split(" - name: Publish dependency release", 1)[1] + publish_script = publish_script.split(" run: |", 1)[1] + self.assertNotIn("${{ inputs.", publish_script) + + def test_bundle_ci_runs_when_its_installer_or_contract_changes(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + for path in ("dikte/ggml.py", "tests/test_packaging.py", + "README.md", "README.tr.md"): + self.assertIn(f"- {path}", workflow) + + def test_the_validator_checks_tar_links_before_extraction(self): + validator = (PACKAGING / "validate-package.sh").read_text( + encoding="utf-8") + for check in ("member.issym()", "member.islnk()", "member.isdev()"): + self.assertIn(check, validator) + + @unittest.skipUnless(sys.platform == "linux" and shutil.which("bash"), + "Linux packaging test is unavailable") + def test_the_validator_rejects_an_escaping_symlink(self): + asset = "whisper-bin-ubuntu-vulkan-x64" + with tempfile.TemporaryDirectory() as temporary: + output = pathlib.Path(temporary) + archive = output / f"{asset}.tar.gz" + with tarfile.open(archive, "w:gz") as bundle: + link = tarfile.TarInfo(f"{asset}/whisper-server") + link.type = tarfile.SYMTYPE + link.linkname = "/etc/passwd" + bundle.addfile(link, io.BytesIO()) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + (output / f"{asset}.tar.gz.sha256").write_text( + f"{digest} {asset}.tar.gz\n", encoding="utf-8", + ) + checked = subprocess.run( + ["bash", PACKAGING / "validate-package.sh"], + env=os.environ | {"OUT_DIR": str(output)}, + capture_output=True, text=True, + ) + self.assertNotEqual(0, checked.returncode) + self.assertIn("unsafe symlink", checked.stderr) + + def test_the_validator_checks_elf_architecture_dependencies_and_paths(self): + validator = (PACKAGING / "validate-package.sh").read_text( + encoding="utf-8") + for check in ("Advanced Micro Devices X86-64", "unexpected DT_NEEDED", + "path.read_bytes()"): + self.assertIn(check, validator) + + def test_the_builder_and_its_downloads_are_pinned(self): + dockerfile = (PACKAGING / "Dockerfile.build").read_text( + encoding="utf-8") + self.assertRegex(dockerfile, r"FROM ubuntu@sha256:[0-9a-f]{64}") + self.assertIn("CMAKE_SHA256=", dockerfile) + self.assertIn("libvulkan-dev=", dockerfile) + self.assertIn("shaderc=", dockerfile) + key = (PACKAGING / "lunarg-signing-key-pub.asc").read_bytes() + self.assertEqual( + "aa1c3c29673140e77f0d6a9aaeed5d9b5621e305ead51c59fae4458bbb4df92b", + hashlib.sha256(key).hexdigest(), + ) + + def test_the_bundle_has_portable_dynamic_backends(self): + script = (PACKAGING / "build-package.sh").read_text( + encoding="utf-8") + for flag in ("GGML_BACKEND_DL=ON", "GGML_CPU_ALL_VARIANTS=ON", + "GGML_NATIVE=OFF", "GGML_OPENMP=OFF", + "GGML_VULKAN=ON"): + self.assertIn(flag, script) + self.assertIn("libggml-cpu*.so", script) + self.assertIn("libggml-vulkan.so", script) + + def test_the_dependency_release_matches_the_installer(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + script = (PACKAGING / "build-package.sh").read_text( + encoding="utf-8") + self.assertEqual("whisper.cpp-v1.9.3", + ggml.MANAGED_WHISPER_RELEASE) + self.assertEqual("v1.9.3", ggml.MANAGED_WHISPER_VERSION) + self.assertIn("RELEASE_TAG: whisper.cpp-v${{ inputs.whisper_version }}", + workflow) + self.assertIn("WHISPER_VERSION:=1.9.3", script) + commit = "371b5a7561823ab2bb32142d2751e35e7534727b" + self.assertIn(f"WHISPER_COMMIT:={commit}", script) + self.assertIn(commit, workflow) + self.assertIn(ggml.MANAGED_WHISPER_VULKAN, workflow) + self.assertIn(ggml.MANAGED_WHISPER_SHA256, workflow) + + def test_the_bundle_carries_metadata_and_all_required_licenses(self): + script = (PACKAGING / "build-package.sh").read_text( + encoding="utf-8") + for name in ("BUILD-INFO.json", "SHA256SUMS", ".cdx.json"): + self.assertIn(name, script) + for name in ("cpp-httplib-MIT.txt", "nlohmann-json-MIT.txt"): + self.assertTrue((PACKAGING / "licenses" / name).is_file()) + + def _make_test_sbom(self): + with tempfile.TemporaryDirectory() as temporary: + root = pathlib.Path(temporary) + (root / "whisper-server").write_bytes(b"elf") + sbom = root / "whisper-bin-ubuntu-vulkan-x64.cdx.json" + environment = os.environ | { + "ROOT": str(root), + "VERSION": "1.9.3", + "COMMIT": "371b5a7561823ab2bb32142d2751e35e7534727b", + "EPOCH": "1787219223", + } + with sbom.open("w", encoding="utf-8") as output: + subprocess.run( + [sys.executable, PACKAGING / "make-sbom.py"], + env=environment, stdout=output, check=True, + ) + return json.loads(sbom.read_text(encoding="utf-8")), sbom.name + + def test_the_sbom_does_not_record_the_file_being_written(self): + document, sbom_name = self._make_test_sbom() + names = {component["name"] for component in document["components"]} + self.assertNotIn(sbom_name, names) + + def test_the_sbom_lists_ggml(self): + document, _ = self._make_test_sbom() + names = {component["name"] for component in document["components"]} + self.assertIn("ggml", names) + + +if __name__ == "__main__": + unittest.main() From 1f57455a3450baaadeea77749777eee8ada2bd03 Mon Sep 17 00:00:00 2001 From: nomoreshow <45514669+nomoreshow@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:04:36 +0300 Subject: [PATCH 2/9] Fix cross-platform test assumptions --- tests/test_ggml.py | 1 + tests/test_packaging.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_ggml.py b/tests/test_ggml.py index 94a69b5..e97254f 100644 --- a/tests/test_ggml.py +++ b/tests/test_ggml.py @@ -205,6 +205,7 @@ class InstallProgram(Local): # These fixtures are Ubuntu release archives. Keep checking that path # on every host, including the Mac that checks the macOS backend. self.patch_attr(sys, "platform", "linux") + self.patch_attr(ggml.platform, "machine", lambda: "x86_64") # Built once, because the release listing has to publish its checksum # and a tarball is not the same bytes twice. self.archive = tarball({ diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 2d63e6e..f9addff 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -21,7 +21,8 @@ WORKFLOW = ROOT / ".github" / "workflows" / "whisper-vulkan.yml" class WhisperVulkanPackaging(unittest.TestCase): - @unittest.skipUnless(shutil.which("bash"), "bash is unavailable") + @unittest.skipUnless(sys.platform != "win32" and shutil.which("bash"), + "bash syntax check is unavailable") def test_the_release_scripts_parse_as_shell(self): for name in ("build-package.sh", "validate-package.sh", "smoke-runtime.sh"): @@ -108,6 +109,7 @@ class WhisperVulkanPackaging(unittest.TestCase): self.assertIn("libvulkan-dev=", dockerfile) self.assertIn("shaderc=", dockerfile) key = (PACKAGING / "lunarg-signing-key-pub.asc").read_bytes() + key = key.replace(b"\r\n", b"\n") self.assertEqual( "aa1c3c29673140e77f0d6a9aaeed5d9b5621e305ead51c59fae4458bbb4df92b", hashlib.sha256(key).hexdigest(), From 956c3eaf3c1ec21e0ae133330eed2d8bc88e86ec Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:16:49 +0300 Subject: [PATCH 3/9] Leave the tar extraction fallback where it was Refusing the install on a Python without the extraction filters is a change to how every archive on every platform is unpacked, and it has nothing to do with shipping a Vulkan whisper-server. On those Pythons the download stops working altogether, which is a worse answer than the one that was there. Worth doing on its own terms, in its own change, where the versions it turns away can be argued about without a backend release riding on it. --- dikte/ggml.py | 7 ++----- tests/test_ggml.py | 12 ------------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/dikte/ggml.py b/dikte/ggml.py index 792a72d..00e9929 100644 --- a/dikte/ggml.py +++ b/dikte/ggml.py @@ -353,11 +353,8 @@ def _extract(archive, into): with tarfile.open(archive, "r:gz") as tar: try: tar.extractall(into, filter="data") - except TypeError as exc: # Python 3.11.0-3 lack extraction filters - raise LocalError(t( - "Could not safely unpack {name} with this Python version", - name=os.path.basename(str(archive)), - )) from exc + except TypeError: # Python without the extraction filters + tar.extractall(into) except (tarfile.TarError, zipfile.BadZipFile, OSError) as exc: raise LocalError(t("Could not unpack {name}: {error}", name=os.path.basename(str(archive)), error=exc)) from exc diff --git a/tests/test_ggml.py b/tests/test_ggml.py index e97254f..184f6fc 100644 --- a/tests/test_ggml.py +++ b/tests/test_ggml.py @@ -500,18 +500,6 @@ class InstallProgram(Local): with self.assertRaises(ggml.LocalError): self.install("whisper-bin-ubuntu-x64.tar.gz", archive=buf.getvalue()) - def test_python_without_safe_tar_filters_refuses_the_archive(self): - archive = self.path("bundle.tar.gz") - archive.write_bytes(self.archive) - destination = self.path("unpacked") - destination.mkdir() - with mock.patch.object(tarfile.TarFile, "extractall", - side_effect=[TypeError("no filter"), None]) as extract: - with self.assertRaises(ggml.LocalError) as caught: - ggml._extract(archive, destination) - self.assertEqual(1, extract.call_count) - self.assertIn("safely", str(caught.exception)) - def test_everything_is_asked_for_over_tls(self): for url in (hub.GITHUB_API, hub.HF_API, hub.HF_FILES): with self.subTest(url=url): From 59180b8efff18736cab98811744f003f73b3cfd4 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:16:59 +0300 Subject: [PATCH 4/9] Say when the processor build landed instead of the Vulkan one The install falls back to upstream's CPU archive whenever Dikte's own release, the file in it, or its reviewed digest is not there, and until the package is published by hand that is every download. It happened without a word: the window said "Downloaded, version v1.9.3." either way, and a graphics card sitting idle looks exactly like one being used. The install record now carries which of the two builds landed, written only where both were on offer, and the settings window says so on the line that already reports the version. --- dikte/ggml.py | 42 ++++++++++++++++++++++++++++++++++++------ dikte/i18n.py | 3 +++ dikte/settings_ui.py | 7 +++++++ tests/test_ggml.py | 19 +++++++++++++++++++ tests/test_ui.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/dikte/ggml.py b/dikte/ggml.py index 00e9929..fd863b9 100644 --- a/dikte/ggml.py +++ b/dikte/ggml.py @@ -284,6 +284,17 @@ def _wanted_assets(program): return (f"bin-ubuntu-{arch}.tar.gz",) +def _managed_whisper(program, tag=""): + """Whether this machine is one Dikte publishes its own whisper-server for. + + Linux x86_64 with a Vulkan loader on it, and no version asked for by hand: + a pinned version is upstream's to answer. + """ + return (not tag and program is WHISPER and sys.platform == "linux" + and platform.machine().lower() in ("x86_64", "amd64") + and _has_vulkan()) + + def _install_record(program): return BIN_DIR / program.name / "installed.json" @@ -307,6 +318,21 @@ def installed_version(program): return _read_record(program).get("tag") or "" +def vulkan_missing(program): + """Whether what Dikte installed is the processor build on a machine the + Vulkan one was fetched for. + + The Vulkan whisper-server is a release of Dikte's own, published by hand + once the reviewed archive is built, and the install falls back to the + upstream processor build whenever that release, the file in it, or its + reviewed digest is not there. Nothing is wrong with the fallback except + that it is invisible: a graphics card sitting idle looks exactly like a + graphics card being used. + """ + return (bool(installed_program(program)) + and _read_record(program).get("backend") == "processor") + + def program_path(program, custom=""): """Which copy of the program to run, or "" when there is none. @@ -382,10 +408,8 @@ def install_program(program, tag="", on_progress=None, should_stop=None, """ repo = program.repo release_tag = tag or "latest" - managed = (not tag and program is WHISPER and sys.platform == "linux" - and platform.machine().lower() in ("x86_64", "amd64") - and _has_vulkan()) - item = None + managed = _managed_whisper(program, tag) + item, vulkan = None, False if managed: repo = DIKTE_REPO release_tag = MANAGED_WHISPER_RELEASE @@ -398,6 +422,7 @@ def install_program(program, tag="", on_progress=None, should_stop=None, item = next((a for a in assets if a.name.endswith(MANAGED_WHISPER_VULKAN) and a.sha256 == MANAGED_WHISPER_SHA256), None) + vulkan = item is not None if item: tag = MANAGED_WHISPER_VERSION @@ -476,8 +501,13 @@ def install_program(program, tag="", on_progress=None, should_stop=None, # Found under the sibling, run from the final directory. binary = into / binary.relative_to(fresh) # Written last, so the record never points at anything half-made. - _install_record(program).write_text( - json.dumps({"tag": tag, "binary": str(binary)}), encoding="utf-8") + record = {"tag": tag, "binary": str(binary)} + if managed: + # Which of the two builds this machine ended up with. Only written + # where both were on offer, so an install that never had the + # choice is not made to look like a fallback. + record["backend"] = "vulkan" if vulkan else "processor" + _install_record(program).write_text(json.dumps(record), encoding="utf-8") except OSError as exc: raise LocalError(t("Could not install {name}: {error}", name=program.name, error=exc)) from exc diff --git a/dikte/i18n.py b/dikte/i18n.py index 23ff6ff..fc7476e 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -780,6 +780,9 @@ TR = { "Not installed.": "Kurulu değil.", "Installed on the system: {path}": "Sistemde kurulu: {path}", "Downloaded, version {version}.": "İndirildi, sürüm {version}.", + "Downloaded, version {version}. There was no Vulkan build, " + "so this one runs on the processor.": + "İndirildi, sürüm {version}. Vulkan sürümü yoktu, bu sürüm işlemcide çalışıyor.", "Fetching the model list…": "Model listesi çekiliyor…", "Downloading…": "İndiriliyor…", "Downloading: {done} of {total}{share}": "İndiriliyor: {done} / {total}{share}", diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 56771f8..c3155da 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -358,6 +358,13 @@ class LocalModelBox(QGroupBox): # for this machine and may reach the graphics card, while the # released binaries carry processor backends only. self.program_label.setText(t("Installed on the system: {path}", path=path)) + elif ggml.vulkan_missing(self.program): + # The download landed the processor build where the graphics card + # one belongs, and nothing else on this window would say so. + self.program_label.setText( + t("Downloaded, version {version}. There was no Vulkan build, " + "so this one runs on the processor.", + version=ggml.installed_version(self.program) or "?")) else: self.program_label.setText( t("Downloaded, version {version}.", diff --git a/tests/test_ggml.py b/tests/test_ggml.py index 184f6fc..8dcfa0c 100644 --- a/tests/test_ggml.py +++ b/tests/test_ggml.py @@ -259,6 +259,7 @@ class InstallProgram(Local): "whisper-bin-ubuntu-vulkan-x64.tar.gz")) self.assertTrue(os.path.isfile(path)) self.assertEqual("v1.9.3", ggml.installed_version(ggml.WHISPER)) + self.assertFalse(ggml.vulkan_missing(ggml.WHISPER)) def test_an_explicit_whisper_version_still_comes_from_upstream(self): self.patch_attr(ggml, "_arch", lambda: "x64") @@ -339,6 +340,24 @@ class InstallProgram(Local): "whisper-bin-ubuntu-x64.tar.gz")) self.assertTrue(os.path.isfile(path)) + def test_a_fallback_to_the_processor_build_is_there_to_be_shown(self): + """Until the Vulkan package is published every download lands the + processor build, and a graphics card sitting idle looks exactly like + one being used. The window asks this and says so.""" + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + managed = self.release("Dikte-1.1.0-x86_64.AppImage") + managed["tag_name"] = "whisper.cpp-v1.9.3" + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(managed, upstream, body(self.archive)): + ggml.install_program(ggml.WHISPER) + self.assertTrue(ggml.vulkan_missing(ggml.WHISPER)) + + def test_a_machine_with_no_vulkan_is_not_told_it_is_missing_one(self): + # Nothing was on offer to fall back from, so there is nothing to say. + self.install("whisper-bin-ubuntu-x64.tar.gz") + self.assertFalse(ggml.vulkan_missing(ggml.WHISPER)) + def test_a_release_with_nothing_for_this_machine_says_so(self): self.patch_attr(ggml, "_arch", lambda: "x64") with fake_urlopen(self.release("whisper-bin-Win32.zip")): diff --git a/tests/test_ui.py b/tests/test_ui.py index 6273447..e147068 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -6,6 +6,7 @@ save, so a setting added to one half and not the other is silently reset the next time anybody presses Save. That is the failure this catches. """ +import json import os import sys import unittest @@ -1214,6 +1215,34 @@ class LocalModels(DikteTest): for row in range(box.repo.count())) self.assertGreaterEqual(view.minimumWidth(), widest) + def test_a_processor_build_where_the_vulkan_one_belongs_says_so(self): + # The Vulkan whisper-server is published by hand, and until it is + # there the download lands upstream's processor build. Said nowhere, + # an idle graphics card looks exactly like one that is being used. + binary = self.path("bin/whisper/v1.9.3/whisper-server") + binary.parent.mkdir(parents=True) + binary.write_text("") + binary.chmod(0o755) + self.path("bin/whisper/installed.json").write_text(json.dumps( + {"tag": "v1.9.3", "binary": str(binary), "backend": "processor"})) + # A whisper-server on this machine's PATH would win over the download. + self.patch_attr(ggml.shutil, "which", lambda name: None) + label = self.window(cfg.Config()).local_whisper.program_label.text() + self.assertIn("v1.9.3", label) + self.assertIn("Vulkan", label) + + def test_an_ordinary_install_is_reported_without_a_word_about_vulkan(self): + binary = self.path("bin/whisper/v1.9.3/whisper-server") + binary.parent.mkdir(parents=True) + binary.write_text("") + binary.chmod(0o755) + self.path("bin/whisper/installed.json").write_text(json.dumps( + {"tag": "v1.9.3", "binary": str(binary)})) + self.patch_attr(ggml.shutil, "which", lambda name: None) + label = self.window(cfg.Config()).local_whisper.program_label.text() + self.assertIn("v1.9.3", label) + self.assertNotIn("Vulkan", label) + def test_only_the_chosen_transcriber_is_on_screen(self): window = self.window(self.config(transcribe_provider="openai")) self.assertTrue(window.stt_form.isRowVisible(window.transcribe_model_row)) From c3bf328eef10c619ba3e10b08095aca49f3300b0 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:37:08 +0300 Subject: [PATCH 5/9] Build the bundle only for what the bundle is built from The trigger listed dikte/ggml.py, both test files and the two READMEs, which are among the files that change most often. A typo fix in the README started a run that builds a container image, compiles every Vulkan shader and starts two more containers, with a 45 minute timeout on it because that is roughly what it costs. Nothing is lost. What ties ggml.py to the release, the tag, version, commit and reviewed digest, is asserted in tests/test_packaging.py, and that file already runs on every pull request in milliseconds. --- .github/workflows/whisper-vulkan.yml | 9 ++++----- tests/test_packaging.py | 15 +++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/whisper-vulkan.yml b/.github/workflows/whisper-vulkan.yml index e3bdf05..4e76d0c 100644 --- a/.github/workflows/whisper-vulkan.yml +++ b/.github/workflows/whisper-vulkan.yml @@ -1,15 +1,14 @@ name: whisper.cpp Vulkan bundle +# Only what the bundle is built from. Compiling the Vulkan shaders takes +# tens of minutes, and a README typo is not worth one: what ties ggml.py to +# this release is a handful of assertions in tests/test_packaging.py, and +# those run on every pull request in milliseconds. on: pull_request: paths: - packaging/whisper-vulkan/** - .github/workflows/whisper-vulkan.yml - - dikte/ggml.py - - tests/test_ggml.py - - tests/test_packaging.py - - README.md - - README.tr.md workflow_dispatch: inputs: whisper_version: diff --git a/tests/test_packaging.py b/tests/test_packaging.py index f9addff..9bffa10 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -58,11 +58,18 @@ class WhisperVulkanPackaging(unittest.TestCase): publish_script = publish_script.split(" run: |", 1)[1] self.assertNotIn("${{ inputs.", publish_script) - def test_bundle_ci_runs_when_its_installer_or_contract_changes(self): + def test_bundle_ci_runs_only_for_what_the_bundle_is_built_from(self): + """A 45 minute build on a README typo is a tax on every other change. + + What ties ggml.py to the release is checked in this file instead, and + this file runs on every pull request in milliseconds.""" workflow = WORKFLOW.read_text(encoding="utf-8") - for path in ("dikte/ggml.py", "tests/test_packaging.py", - "README.md", "README.tr.md"): - self.assertIn(f"- {path}", workflow) + trigger = workflow.split("workflow_dispatch:", 1)[0] + self.assertIn("- packaging/whisper-vulkan/**", trigger) + self.assertIn("- .github/workflows/whisper-vulkan.yml", trigger) + for path in ("dikte/ggml.py", "tests/test_ggml.py", + "tests/test_packaging.py", "README.md", "README.tr.md"): + self.assertNotIn(f"- {path}", trigger) def test_the_validator_checks_tar_links_before_extraction(self): validator = (PACKAGING / "validate-package.sh").read_text( From 00a5283adb9aedcd18129d05318011f3fa9bfc0f Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:45:00 +0300 Subject: [PATCH 6/9] Let a downloaded program be downloaded again The button disappeared the moment anything landed, and nothing else on the window asks for that download. So a machine that got the processor build before its graphics driver was installed can never be given the Vulkan one, and nobody can pick up a newer whisper.cpp either: both of those are the same missing control. It reads "Download again" once a copy is here, and stays hidden while a system one is on the PATH, because that is the copy that would run. --- dikte/i18n.py | 1 + dikte/settings_ui.py | 12 ++++++++++-- tests/test_ui.py | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/dikte/i18n.py b/dikte/i18n.py index fc7476e..7233c74 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -779,6 +779,7 @@ TR = { "Local model": "Yerel model", "Not installed.": "Kurulu değil.", "Installed on the system: {path}": "Sistemde kurulu: {path}", + "Download again": "Yeniden indir", "Downloaded, version {version}.": "İndirildi, sürüm {version}.", "Downloaded, version {version}. There was no Vulkan build, " "so this one runs on the processor.": diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index c3155da..978df52 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -349,10 +349,18 @@ class LocalModelBox(QGroupBox): path = ggml.program_path(self.program) if not path: self.program_label.setText(t("Not installed.")) + self.install_button.setText(t("Download")) self.install_button.setVisible(True) return - self.install_button.setVisible(not ggml.installed_program(self.program) - and not ggml.system_program(self.program)) + # A copy that is here is not a copy that is right. whisper.cpp releases + # every few weeks, and a graphics card installed after Dikte was + # changes which build this machine should be running; the button was + # hidden the moment anything landed, and nothing else on this window + # asks for the download again. + self.install_button.setText(t("Download again") + if ggml.installed_program(self.program) + else t("Download")) + self.install_button.setVisible(not ggml.system_program(self.program)) if ggml.system_program(self.program): # Worth saying which one is running: a distribution package is built # for this machine and may reach the graphics card, while the diff --git a/tests/test_ui.py b/tests/test_ui.py index e147068..ed86910 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -22,6 +22,7 @@ from dikte import cleanup from dikte import config as cfg from dikte import ggml from dikte import hotkey +from dikte.i18n import t from dikte import ipc from dikte import overlay as overlay_module from dikte import paste @@ -1243,6 +1244,27 @@ class LocalModels(DikteTest): self.assertIn("v1.9.3", label) self.assertNotIn("Vulkan", label) + def test_a_downloaded_program_can_still_be_asked_for_again(self): + # The button used to disappear the moment anything landed, which left + # no way to pick up a newer whisper.cpp, or the Vulkan build on a + # machine whose driver was installed after Dikte was. + binary = self.path("bin/whisper/v1.9.3/whisper-server") + binary.parent.mkdir(parents=True) + binary.write_text("") + binary.chmod(0o755) + self.path("bin/whisper/installed.json").write_text(json.dumps( + {"tag": "v1.9.3", "binary": str(binary)})) + self.patch_attr(ggml.shutil, "which", lambda name: None) + box = self.window(cfg.Config()).local_whisper + self.assertTrue(box.install_button.isVisibleTo(box)) + self.assertEqual(box.install_button.text(), t("Download again")) + + def test_a_system_copy_is_not_offered_for_download(self): + # Nothing Dikte downloads would be run while one is on the PATH. + self.patch_attr(ggml.shutil, "which", lambda name: "/usr/bin/" + name) + box = self.window(cfg.Config()).local_whisper + self.assertFalse(box.install_button.isVisibleTo(box)) + def test_only_the_chosen_transcriber_is_on_screen(self): window = self.window(self.config(transcribe_provider="openai")) self.assertTrue(window.stt_form.isRowVisible(window.transcribe_model_row)) From 10ef4e62a9b9631607c77ee18d53142055e3c700 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:45:19 +0300 Subject: [PATCH 7/9] Smoke-test the bundle the way Dikte starts it The CPU run passed -ng, which tells whisper.cpp not to look for a backend at all, so the claim the bundle rests on, that it starts and transcribes where Vulkan cannot, was never tested. Dikte passes -ng only when its GPU setting is off; every other run goes through the backend registry. Both runs lose the flag, and a third one is added between them: the loader installed, nothing behind it. That is a real machine, libvulkan pulled in by something else with no driver to go with it, and it is the one whose answer nobody knew. --- .github/workflows/whisper-vulkan.yml | 3 +++ .../whisper-vulkan/Dockerfile.runtime-noicd | 9 +++++++++ packaging/whisper-vulkan/smoke-runtime.sh | 20 +++++++++++++++---- tests/test_packaging.py | 14 +++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 packaging/whisper-vulkan/Dockerfile.runtime-noicd diff --git a/.github/workflows/whisper-vulkan.yml b/.github/workflows/whisper-vulkan.yml index 4e76d0c..ef4a28c 100644 --- a/.github/workflows/whisper-vulkan.yml +++ b/.github/workflows/whisper-vulkan.yml @@ -110,6 +110,9 @@ jobs: - name: CPU fallback smoke test (no Vulkan loader) run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh cpu + - name: Vulkan loader present, no device smoke test + run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh noicd + - name: Vulkan plugin-load smoke test (Mesa llvmpipe) run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh vulkan diff --git a/packaging/whisper-vulkan/Dockerfile.runtime-noicd b/packaging/whisper-vulkan/Dockerfile.runtime-noicd new file mode 100644 index 0000000..bda8b71 --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.runtime-noicd @@ -0,0 +1,9 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc +ARG DEBIAN_FRONTEND=noninteractive +# The loader and nothing behind it: the machine that has libvulkan because +# something else pulled it in, and no driver to go with it. +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + ca-certificates curl libstdc++6 libvulkan1 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /bundle diff --git a/packaging/whisper-vulkan/smoke-runtime.sh b/packaging/whisper-vulkan/smoke-runtime.sh index 22c0626..ac71319 100755 --- a/packaging/whisper-vulkan/smoke-runtime.sh +++ b/packaging/whisper-vulkan/smoke-runtime.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -mode=${1:?usage: smoke-runtime.sh cpu|vulkan} +mode=${1:?usage: smoke-runtime.sh cpu|noicd|vulkan} : "${OUT_DIR:=work/out}" : "${FIXTURE_SOURCE:=vendor/whisper.cpp}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -10,6 +10,7 @@ FIXTURE_SOURCE="$(realpath "$FIXTURE_SOURCE")" asset=whisper-bin-ubuntu-vulkan-x64 case "$mode" in cpu) dockerfile=Dockerfile.runtime-cpu; image=dikte-whisper-runtime-cpu:spike ;; + noicd) dockerfile=Dockerfile.runtime-noicd; image=dikte-whisper-runtime-noicd:spike ;; vulkan) dockerfile=Dockerfile.runtime-vulkan; image=dikte-whisper-runtime-vulkan:spike ;; *) echo "unknown mode: $mode" >&2; exit 2 ;; esac @@ -23,9 +24,10 @@ args=("/bundle/$asset/whisper-server" -m /fixtures/model.bin --host 127.0.0.1 --port 8080 --inference-path /v1/audio/transcriptions -l auto -sns -nlp) env_args=() -if [[ "$mode" == cpu ]]; then - args+=(-ng) -else +# No -ng anywhere: Dikte passes it only when its GPU setting is off, so the +# run that has to survive a missing loader or a missing device is this one, +# where the backend registry actually goes looking for them. +if [[ "$mode" == vulkan ]]; then env_args=(-e LIBGL_ALWAYS_SOFTWARE=1 -e VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json) fi @@ -41,6 +43,16 @@ docker run --rm --name "dikte-whisper-$mode-smoke" \ echo "CPU smoke image unexpectedly has a Vulkan loader" >&2 exit 1 fi + if [ "$SMOKE_MODE" = noicd ]; then + if ! ldconfig -p | grep -q libvulkan.so.1; then + echo "no-ICD smoke image has no Vulkan loader to load" >&2 + exit 1 + fi + if compgen -G "/usr/share/vulkan/icd.d/*.json" >/dev/null; then + echo "no-ICD smoke image has a driver after all" >&2 + exit 1 + fi + fi "$@" >/tmp/server.log 2>&1 & pid=$! trap "kill $pid 2>/dev/null || true" EXIT diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 9bffa10..fdbfcb9 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -39,6 +39,7 @@ class WhisperVulkanPackaging(unittest.TestCase): "Verify reviewed archive digest", "Validate archive and ELF contract", "CPU fallback smoke test (no Vulkan loader)", + "Vulkan loader present, no device smoke test", "Vulkan plugin-load smoke test (Mesa llvmpipe)", "Publish dependency release"): self.assertIn(step, workflow) @@ -71,6 +72,19 @@ class WhisperVulkanPackaging(unittest.TestCase): "tests/test_packaging.py", "README.md", "README.tr.md"): self.assertNotIn(f"- {path}", trigger) + def test_the_smoke_tests_run_what_dikte_runs(self): + """-ng is what Dikte passes when its GPU setting is off, and a run + with it never asks for a backend at all. The three runs that have to + hold are the ones without it: no loader, a loader with nothing behind + it, and a working device.""" + script = (PACKAGING / "smoke-runtime.sh").read_text(encoding="utf-8") + code = "\n".join(line for line in script.splitlines() + if not line.lstrip().startswith("#")) + self.assertNotIn("-ng", code) + for mode in ("cpu)", "noicd)", "vulkan)"): + self.assertIn(mode, script) + self.assertTrue((PACKAGING / "Dockerfile.runtime-noicd").is_file()) + def test_the_validator_checks_tar_links_before_extraction(self): validator = (PACKAGING / "validate-package.sh").read_text( encoding="utf-8") From 5f6e4ad78245dfadfae36ada91412c462b114674 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:45:48 +0300 Subject: [PATCH 8/9] Make a version bump possible without editing the workflow first The digest was a literal in the workflow while the version was an input, so a dispatch for anything but 1.9.3 built the archive and then failed its own gate. The digest of a version nobody has reviewed cannot be known before it is built, which made the inputs unusable for the one job they exist for. An empty expected_sha256 now reports the digest of what was built and refuses to publish; a digest handed in is checked the way the literal was. The input shapes are also checked before the checkout that uses them rather than after it. --- .github/workflows/whisper-vulkan.yml | 49 +++++++++++++++++++++++----- tests/test_packaging.py | 14 ++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/.github/workflows/whisper-vulkan.yml b/.github/workflows/whisper-vulkan.yml index ef4a28c..eb9bc32 100644 --- a/.github/workflows/whisper-vulkan.yml +++ b/.github/workflows/whisper-vulkan.yml @@ -21,6 +21,14 @@ on: required: true default: "371b5a7561823ab2bb32142d2751e35e7534727b" type: string + expected_sha256: + description: >- + Reviewed archive SHA-256. Leave empty for a version this file has + not reviewed: the digest of what was built is reported instead of + being checked, and publishing is refused. + required: false + default: "" + type: string publish: description: Publish a Dikte dependency release required: true @@ -37,7 +45,8 @@ concurrency: env: WHISPER_VERSION: ${{ inputs.whisper_version || '1.9.3' }} WHISPER_COMMIT: ${{ inputs.whisper_commit || '371b5a7561823ab2bb32142d2751e35e7534727b' }} - MANAGED_WHISPER_SHA256: c25ca76504144da488eb74441390a7b9aa7ce547e5f2f391cbd831253c9b54d8 + REVIEWED_WHISPER_VERSION: "1.9.3" + REVIEWED_WHISPER_SHA256: c25ca76504144da488eb74441390a7b9aa7ce547e5f2f391cbd831253c9b54d8 jobs: build: @@ -49,6 +58,13 @@ jobs: with: persist-credentials: false + - name: Validate source coordinates + shell: bash + run: | + set -euo pipefail + [[ "$WHISPER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "$WHISPER_COMMIT" =~ ^[0-9a-f]{40}$ ]] + - name: Check out pinned whisper.cpp source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: @@ -58,13 +74,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Validate source coordinates - shell: bash - run: | - set -euo pipefail - [[ "$WHISPER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] - [[ "$WHISPER_COMMIT" =~ ^[0-9a-f]{40}$ ]] - - name: Verify source version and commit shell: bash run: | @@ -92,9 +101,31 @@ jobs: - name: Verify reviewed archive digest shell: bash + env: + EXPECTED_SHA256: ${{ inputs.expected_sha256 }} + PUBLISH: ${{ inputs.publish }} run: | + set -euo pipefail read -r actual _ < dist/whisper-bin-ubuntu-vulkan-x64.tar.gz.sha256 - test "$actual" = "$MANAGED_WHISPER_SHA256" + echo "built archive sha256: $actual" + expected="$EXPECTED_SHA256" + if [ -z "$expected" ] \ + && [ "$WHISPER_VERSION" = "$REVIEWED_WHISPER_VERSION" ]; then + expected="$REVIEWED_WHISPER_SHA256" + fi + if [ -z "$expected" ]; then + # The digest of a version nobody has reviewed yet cannot be known + # before it is built. Reporting it is the whole point of the run; + # a release out of it is not. + if [ "${PUBLISH:-false}" = true ]; then + echo "refusing to publish an archive whose digest has not been reviewed" >&2 + exit 1 + fi + echo "::notice::no reviewed digest for $WHISPER_VERSION." \ + "Review the one above, then dispatch again with expected_sha256." + exit 0 + fi + test "$actual" = "$expected" - name: Validate archive and ELF contract run: OUT_DIR=dist packaging/whisper-vulkan/validate-package.sh diff --git a/tests/test_packaging.py b/tests/test_packaging.py index fdbfcb9..dde74b6 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -85,6 +85,20 @@ class WhisperVulkanPackaging(unittest.TestCase): self.assertIn(mode, script) self.assertTrue((PACKAGING / "Dockerfile.runtime-noicd").is_file()) + def test_an_unreviewed_version_is_reported_and_never_published(self): + """The digest of a version nobody has reviewed cannot be known before + it is built, so the gate cannot be the only way through.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn("expected_sha256", workflow) + self.assertIn( + "refusing to publish an archive whose digest has not been reviewed", + workflow) + + def test_the_shape_of_the_inputs_is_checked_before_they_are_used(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertLess(workflow.index("- name: Validate source coordinates"), + workflow.index("- name: Check out pinned whisper.cpp")) + def test_the_validator_checks_tar_links_before_extraction(self): validator = (PACKAGING / "validate-package.sh").read_text( encoding="utf-8") From 156d8bf8e8b596b0ef3804b53f0f40a494e486c0 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 09:46:08 +0300 Subject: [PATCH 9/9] Write the release order down where it will be read Enabling immutable releases, configuring the dependency-release environment, which order the two dispatch runs go in, and which five constants have to move together were all in the pull request description, which is not a place anybody looks a year later. The same file says what this costs: the pinned digest means a backend update is a Dikte release, and the build is deterministic between two runs of one builder rather than across time, because the apt metadata behind the pinned packages is not pinned and LunarG drops superseded ones. Both are worth knowing before the next version bump, not during it. The README keeps a clause instead of the paragraph it had grown. --- README.md | 7 ++--- README.tr.md | 5 ++- packaging/whisper-vulkan/README.md | 49 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 packaging/whisper-vulkan/README.md diff --git a/README.md b/README.md index 601311e..048b652 100644 --- a/README.md +++ b/README.md @@ -163,10 +163,9 @@ running. the program and the model, verifies the sha256 and refuses a download published without one, then keeps a server alive while you dictate. The graphics card is reached through CUDA, ROCm or Vulkan where the build allows. No key, no - account, nothing leaving the machine. On x86_64 Linux, that same Download - button tries the reviewed Vulkan bundle when a loader is present, and falls - back to upstream's CPU build if it is unavailable. The bundle also carries CPU - backends for systems where a Vulkan device cannot start. + account, nothing leaving the machine. On x86_64 Linux the same button fetches + a Vulkan build of whisper-server that Dikte publishes itself, because + upstream's Linux archive is processor-only. - **Silence never reaches the API.** Handed near-silence, a transcription model invents a sentence instead of returning nothing ("Thanks for watching", or in Turkish "Altyazı M.K."). A recording is dropped when nothing rose 10 dB above diff --git a/README.tr.md b/README.tr.md index 0fc592f..0fb43e4 100644 --- a/README.tr.md +++ b/README.tr.md @@ -160,9 +160,8 @@ olmasını ister. checksum'suz yayınlanmış bir indirmeyi reddeder, sen dikte ettikçe sunucuyu ayakta tutar. Derleme destekliyorsa ekran kartına CUDA, ROCm ya da Vulkan üzerinden ulaşılır. Anahtar yok, hesap yok, makineden çıkan bir şey yok. - x86_64 Linux'ta aynı İndir düğmesi, Vulkan yükleyicisi varsa incelenmiş Vulkan - paketini dener; paket kullanılamıyorsa upstream'in işlemci derlemesine döner. - Vulkan aygıtı başlatılamadığında kullanılacak işlemci arka uçları da pakettedir. + x86_64 Linux'ta aynı düğme, whisper-server'ın Dikte'nin kendi yayınladığı + Vulkan derlemesini indirir; upstream'in Linux arşivi yalnızca işlemci için. - **Sessizlik API'ye gitmez.** Sessize yakın bir ses verildiğinde model boş dize döndürmez, bir cümle uydurur ("Altyazı M.K.", "Thanks for watching"). *O kaydın kendi* gürültü tabanının 10 dB üstüne en az 0,3 saniye çıkan bir şey diff --git a/packaging/whisper-vulkan/README.md b/packaging/whisper-vulkan/README.md new file mode 100644 index 0000000..371b7c9 --- /dev/null +++ b/packaging/whisper-vulkan/README.md @@ -0,0 +1,49 @@ +# The Vulkan whisper-server bundle + +whisper.cpp publishes a CPU-only archive for Linux, so the graphics card on a +Linux machine is out of reach through the Download button. This directory +builds the archive upstream does not: `whisper-server` with a dynamic Vulkan +backend next to the CPU ones, for x86_64, against the Ubuntu 22.04 runtime +contract. + +It is published as a release of Dikte's own, `whisper.cpp-v`, marked +as a prerelease and kept off Latest so that neither the update check nor the +download page picks it up. `dikte/ggml.py` fetches it by tag and installs it +only when the archive's digest is the reviewed one; anything else falls back +to upstream's CPU archive, and the settings window says when it did. + +## Publishing a new bundle + +1. Enable GitHub's immutable releases setting for the repository, and give the + `dependency-release` environment a required reviewer. Both are repository + settings, not something this workflow can do for itself. +2. Run **whisper.cpp Vulkan bundle** on `master` with the new version and its + peeled commit, `expected_sha256` empty and `publish: false`. The run builds + the archive and reports its digest; without a reviewed digest it refuses to + publish, which is what the first run is for. +3. Review that digest against a build of your own, then run the workflow again + with the same version and commit, `expected_sha256` set to it, and + `publish: true`. Approve the environment when it asks. +4. Write the same version, tag and digest into `MANAGED_WHISPER_RELEASE`, + `MANAGED_WHISPER_VERSION` and `MANAGED_WHISPER_SHA256` in `dikte/ggml.py`, + and into `REVIEWED_WHISPER_VERSION` and `REVIEWED_WHISPER_SHA256` in the + workflow. `tests/test_packaging.py` holds the two sides together. +5. Ship a Dikte release. Until one goes out, nobody's Dikte knows the new + bundle exists. + +## What this costs, and what it does not promise + +The digest lives in Dikte's source, so a backend update is a Dikte release. +Linux x86_64 machines with a Vulkan loader stay on the pinned whisper.cpp +version until step 5 happens, while every other platform follows upstream's +newest release on its own. That is the deliberate trade: an executable Dikte +downloads is not allowed to change without a reviewed digest behind it. + +The build is deterministic between two runs of the same builder, not across +time. The base image, the CMake tarball, the LunarG packages and the direct +apt packages are pinned by digest or version, but the Ubuntu and LunarG +repository metadata behind them is not, and LunarG drops superseded packages. +A rebuild months later can fail to resolve, or resolve to something that +produces a different digest. Treat the published archive as the artifact, not +as something reproducible on demand: a version bump means building, +validating, reviewing the new digest and updating the pinned tuple together.