mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Merge pull request #70 from nomoreshow/feat/managed-vulkan-whisper
Ship a managed Vulkan whisper-server for Linux x64
This commit is contained in:
@@ -0,0 +1,222 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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' }}
|
||||||
|
REVIEWED_WHISPER_VERSION: "1.9.3"
|
||||||
|
REVIEWED_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: 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:
|
||||||
|
repository: ggml-org/whisper.cpp
|
||||||
|
ref: ${{ env.WHISPER_COMMIT }}
|
||||||
|
path: vendor/whisper.cpp
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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 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
|
||||||
|
|
||||||
|
- 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"
|
||||||
@@ -163,7 +163,9 @@ running.
|
|||||||
the program and the model, verifies the sha256 and refuses a download published
|
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
|
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
|
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 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
|
- **Silence never reaches the API.** Handed near-silence, a transcription model
|
||||||
invents a sentence instead of returning nothing ("Thanks for watching", or in
|
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
|
Turkish "Altyazı M.K."). A recording is dropped when nothing rose 10 dB above
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ olmasını ister.
|
|||||||
checksum'suz yayınlanmış bir indirmeyi reddeder, sen dikte ettikçe sunucuyu
|
checksum'suz yayınlanmış bir indirmeyi reddeder, sen dikte ettikçe sunucuyu
|
||||||
ayakta tutar. Derleme destekliyorsa ekran kartına CUDA, ROCm ya da Vulkan
|
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.
|
üzerinden ulaşılır. Anahtar yok, hesap yok, makineden çıkan bir şey yok.
|
||||||
|
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
|
- **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
|
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
|
kaydın kendi* gürültü tabanının 10 dB üstüne en az 0,3 saniye çıkan bir şey
|
||||||
|
|||||||
+71
-3
@@ -76,6 +76,13 @@ Program = collections.namedtuple("Program", "name repo binary health")
|
|||||||
|
|
||||||
WHISPER = Program("whisper", "ggml-org/whisper.cpp", "whisper-server", "")
|
WHISPER = Program("whisper", "ggml-org/whisper.cpp", "whisper-server", "")
|
||||||
LLAMA = Program("llama", "ggml-org/llama.cpp", "llama-server", "/health")
|
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
|
# 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
|
# in the source means a release of Dikte for every model somebody else
|
||||||
@@ -281,6 +288,35 @@ def _wanted_assets(program):
|
|||||||
return (f"bin-ubuntu-{arch}.tar.gz",)
|
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 _managed_asset(refresh=False):
|
||||||
|
"""The Vulkan whisper-server Dikte builds itself, or None.
|
||||||
|
|
||||||
|
Taken only when the archive's digest is the reviewed one. Anything else,
|
||||||
|
a release that is not there yet, a GitHub that cannot be reached, a file
|
||||||
|
that is not the reviewed bytes, leaves upstream's processor build as the
|
||||||
|
answer, and the install record says which of the two landed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
_, assets = hub.release(DIKTE_REPO, MANAGED_WHISPER_RELEASE,
|
||||||
|
refresh=refresh)
|
||||||
|
except hub.HubError:
|
||||||
|
return None
|
||||||
|
return next((a for a in assets
|
||||||
|
if a.name.endswith(MANAGED_WHISPER_VULKAN)
|
||||||
|
and a.sha256 == MANAGED_WHISPER_SHA256), None)
|
||||||
|
|
||||||
|
|
||||||
def _matching_asset(program, assets):
|
def _matching_asset(program, assets):
|
||||||
"""The archive this machine wants out of one release's files, or None."""
|
"""The archive this machine wants out of one release's files, or None."""
|
||||||
for ending in _wanted_assets(program):
|
for ending in _wanted_assets(program):
|
||||||
@@ -294,6 +330,10 @@ def _pick_asset(program, tag="", refresh=False):
|
|||||||
"""(tag, Item) for the release archive to install. Item is None when there
|
"""(tag, Item) for the release archive to install. Item is None when there
|
||||||
is none for this machine.
|
is none for this machine.
|
||||||
|
|
||||||
|
Dikte's own Vulkan whisper-server comes before upstream's where this
|
||||||
|
machine is one it is built for, because whisper.cpp publishes no Vulkan
|
||||||
|
archive for Linux at all.
|
||||||
|
|
||||||
A named tag is taken as given. For the newest, what GitHub answers is not
|
A named tag is taken as given. For the newest, what GitHub answers is not
|
||||||
always where the builds are: llama.cpp's latest release is a version marker
|
always where the builds are: llama.cpp's latest release is a version marker
|
||||||
carrying a single nightly-tag.txt, which names the tag the archives are
|
carrying a single nightly-tag.txt, which names the tag the archives are
|
||||||
@@ -301,6 +341,10 @@ def _pick_asset(program, tag="", refresh=False):
|
|||||||
at. The pointer is followed when it is there, and when it is not, the newest
|
at. The pointer is followed when it is there, and when it is not, the newest
|
||||||
release that does carry a build for this machine is taken instead.
|
release that does carry a build for this machine is taken instead.
|
||||||
"""
|
"""
|
||||||
|
if _managed_whisper(program, tag):
|
||||||
|
item = _managed_asset(refresh=refresh)
|
||||||
|
if item:
|
||||||
|
return MANAGED_WHISPER_VERSION, item
|
||||||
named = bool(tag) and tag != "latest"
|
named = bool(tag) and tag != "latest"
|
||||||
missing = None
|
missing = None
|
||||||
try:
|
try:
|
||||||
@@ -363,6 +407,21 @@ def installed_version(program):
|
|||||||
return _read_record(program).get("tag") or ""
|
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=""):
|
def program_path(program, custom=""):
|
||||||
"""Which copy of the program to run, or "" when there is none.
|
"""Which copy of the program to run, or "" when there is none.
|
||||||
|
|
||||||
@@ -435,8 +494,10 @@ def install_program(program, tag="", on_progress=None, should_stop=None,
|
|||||||
|
|
||||||
`tag` is empty for whatever the project released last, which is the point:
|
`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
|
a version pinned in Dikte's source would mean a release of Dikte every time
|
||||||
whisper.cpp has one.
|
whisper.cpp has one. The Linux Vulkan whisper-server is the exception, and
|
||||||
|
_pick_asset says why.
|
||||||
"""
|
"""
|
||||||
|
managed = _managed_whisper(program, tag)
|
||||||
try:
|
try:
|
||||||
tag, item = _pick_asset(program, tag, refresh=refresh)
|
tag, item = _pick_asset(program, tag, refresh=refresh)
|
||||||
except hub.HubError as exc:
|
except hub.HubError as exc:
|
||||||
@@ -505,8 +566,15 @@ def install_program(program, tag="", on_progress=None, should_stop=None,
|
|||||||
# Found under the sibling, run from the final directory.
|
# Found under the sibling, run from the final directory.
|
||||||
binary = into / binary.relative_to(fresh)
|
binary = into / binary.relative_to(fresh)
|
||||||
# Written last, so the record never points at anything half-made.
|
# Written last, so the record never points at anything half-made.
|
||||||
_install_record(program).write_text(
|
record = {"tag": tag, "binary": str(binary)}
|
||||||
json.dumps({"tag": tag, "binary": str(binary)}), encoding="utf-8")
|
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 item.name.endswith(MANAGED_WHISPER_VULKAN)
|
||||||
|
else "processor")
|
||||||
|
_install_record(program).write_text(json.dumps(record), encoding="utf-8")
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise LocalError(t("Could not install {name}: {error}",
|
raise LocalError(t("Could not install {name}: {error}",
|
||||||
name=program.name, error=exc)) from exc
|
name=program.name, error=exc)) from exc
|
||||||
|
|||||||
@@ -784,7 +784,11 @@ TR = {
|
|||||||
"Local model": "Yerel model",
|
"Local model": "Yerel model",
|
||||||
"Not installed.": "Kurulu değil.",
|
"Not installed.": "Kurulu değil.",
|
||||||
"Installed on the system: {path}": "Sistemde kurulu: {path}",
|
"Installed on the system: {path}": "Sistemde kurulu: {path}",
|
||||||
|
"Download again": "Yeniden indir",
|
||||||
"Downloaded, version {version}.": "İndirildi, sürüm {version}.",
|
"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…",
|
"Fetching the model list…": "Model listesi çekiliyor…",
|
||||||
"Downloading…": "İndiriliyor…",
|
"Downloading…": "İndiriliyor…",
|
||||||
"Downloading: {done} of {total}{share}": "İndiriliyor: {done} / {total}{share}",
|
"Downloading: {done} of {total}{share}": "İndiriliyor: {done} / {total}{share}",
|
||||||
|
|||||||
+17
-2
@@ -358,15 +358,30 @@ class LocalModelBox(QGroupBox):
|
|||||||
path = ggml.program_path(self.program)
|
path = ggml.program_path(self.program)
|
||||||
if not path:
|
if not path:
|
||||||
self.program_label.setText(t("Not installed."))
|
self.program_label.setText(t("Not installed."))
|
||||||
|
self.install_button.setText(t("Download"))
|
||||||
self.install_button.setVisible(True)
|
self.install_button.setVisible(True)
|
||||||
return
|
return
|
||||||
self.install_button.setVisible(not ggml.installed_program(self.program)
|
# A copy that is here is not a copy that is right. whisper.cpp releases
|
||||||
and not ggml.system_program(self.program))
|
# 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):
|
if ggml.system_program(self.program):
|
||||||
# Worth saying which one is running: a distribution package is built
|
# Worth saying which one is running: a distribution package is built
|
||||||
# for this machine and may reach the graphics card, while the
|
# for this machine and may reach the graphics card, while the
|
||||||
# released binaries carry processor backends only.
|
# released binaries carry processor backends only.
|
||||||
self.program_label.setText(t("Installed on the system: {path}", path=path))
|
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:
|
else:
|
||||||
self.program_label.setText(
|
self.program_label.setText(
|
||||||
t("Downloaded, version {version}.",
|
t("Downloaded, version {version}.",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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<version>`, 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.
|
||||||
Executable
+128
@@ -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" <<EOF
|
||||||
|
{
|
||||||
|
"asset": "$asset.tar.gz",
|
||||||
|
"source": "https://github.com/ggml-org/whisper.cpp",
|
||||||
|
"source_version": "v$WHISPER_VERSION",
|
||||||
|
"source_commit": "$WHISPER_COMMIT",
|
||||||
|
"source_date_epoch": $SOURCE_DATE_EPOCH,
|
||||||
|
"build_platform": "ubuntu-22.04-x86_64",
|
||||||
|
"base_image": "ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc",
|
||||||
|
"cmake": "3.31.6",
|
||||||
|
"cmake_flags": [
|
||||||
|
"BUILD_SHARED_LIBS=ON",
|
||||||
|
"C/CXX_FILE_PREFIX_MAP=/work/source=.",
|
||||||
|
"GGML_BACKEND_DL=ON",
|
||||||
|
"GGML_CPU_ALL_VARIANTS=ON",
|
||||||
|
"GGML_NATIVE=OFF",
|
||||||
|
"GGML_CCACHE=OFF",
|
||||||
|
"GGML_OPENMP=OFF",
|
||||||
|
"GGML_VULKAN=ON",
|
||||||
|
"WHISPER_BUILD_EXAMPLES=ON",
|
||||||
|
"WHISPER_BUILD_SERVER=ON",
|
||||||
|
"WHISPER_BUILD_TESTS=OFF",
|
||||||
|
"WHISPER_BUILD_IS_DEV=OFF",
|
||||||
|
"WHISPER_CURL=OFF",
|
||||||
|
"WHISPER_SDL2=OFF",
|
||||||
|
"WHISPER_COMMON_FFMPEG=OFF"
|
||||||
|
],
|
||||||
|
"runtime_contract": {
|
||||||
|
"minimum_glibc": "2.34",
|
||||||
|
"minimum_glibcxx": "3.4.30",
|
||||||
|
"required": ["x86_64 Linux", "glibc", "libstdc++.so.6", "libgcc_s.so.1"],
|
||||||
|
"optional_gpu": ["libvulkan.so.1", "a working Vulkan ICD"],
|
||||||
|
"cpu_fallback": "dynamic CPU backends are included; -ng forces CPU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# A deterministic CycloneDX sidecar generated from the files actually shipped.
|
||||||
|
ROOT="$root" VERSION="$WHISPER_VERSION" COMMIT="$WHISPER_COMMIT" EPOCH="$SOURCE_DATE_EPOCH" \
|
||||||
|
python3 /packaging/make-sbom.py > "$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"
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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-----
|
||||||
Executable
+116
@@ -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/[email protected]"
|
||||||
|
httplib_ref = "pkg:github/yhirose/[email protected]"
|
||||||
|
json_ref = "pkg:github/nlohmann/[email protected]"
|
||||||
|
|
||||||
|
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()
|
||||||
Executable
+76
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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)"
|
||||||
|
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 ;;
|
||||||
|
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
|
||||||
|
|
||||||
|
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=()
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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[@]}"
|
||||||
Executable
+145
@@ -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"
|
||||||
@@ -205,6 +205,7 @@ class InstallProgram(Local):
|
|||||||
# These fixtures are Ubuntu release archives. Keep checking that path
|
# These fixtures are Ubuntu release archives. Keep checking that path
|
||||||
# on every host, including the Mac that checks the macOS backend.
|
# on every host, including the Mac that checks the macOS backend.
|
||||||
self.patch_attr(sys, "platform", "linux")
|
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
|
# Built once, because the release listing has to publish its checksum
|
||||||
# and a tarball is not the same bytes twice.
|
# and a tarball is not the same bytes twice.
|
||||||
self.archive = tarball({
|
self.archive = tarball({
|
||||||
@@ -221,6 +222,7 @@ class InstallProgram(Local):
|
|||||||
|
|
||||||
def install(self, *names, archive=None):
|
def install(self, *names, archive=None):
|
||||||
self.patch_attr(ggml, "_arch", lambda: "x64")
|
self.patch_attr(ggml, "_arch", lambda: "x64")
|
||||||
|
self.patch_attr(ggml, "_has_vulkan", lambda: False)
|
||||||
blob = self.archive if archive is None else archive
|
blob = self.archive if archive is None else archive
|
||||||
with serving(self.release(*names, archive=blob), blob) as calls:
|
with serving(self.release(*names, archive=blob), blob) as calls:
|
||||||
path = ggml.install_program(ggml.WHISPER)
|
path = ggml.install_program(ggml.WHISPER)
|
||||||
@@ -276,6 +278,123 @@ class InstallProgram(Local):
|
|||||||
tag, found = ggml._pick_asset(ggml.LLAMA)
|
tag, found = ggml._pick_asset(ggml.LLAMA)
|
||||||
self.assertEqual(tag, "b1")
|
self.assertEqual(tag, "b1")
|
||||||
self.assertEqual(found.name, "llama-b1-bin-ubuntu-x64.tar.gz")
|
self.assertEqual(found.name, "llama-b1-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))
|
||||||
|
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")
|
||||||
|
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_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):
|
def test_a_release_with_nothing_for_this_machine_says_so(self):
|
||||||
self.patch_attr(ggml, "_arch", lambda: "x64")
|
self.patch_attr(ggml, "_arch", lambda: "x64")
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"""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(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"):
|
||||||
|
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 loader present, no device smoke test",
|
||||||
|
"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_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")
|
||||||
|
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_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_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")
|
||||||
|
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()
|
||||||
|
key = key.replace(b"\r\n", b"\n")
|
||||||
|
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()
|
||||||
@@ -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.
|
next time anybody presses Save. That is the failure this catches.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -28,6 +29,7 @@ from dikte import overlay as overlay_module
|
|||||||
from dikte import paste
|
from dikte import paste
|
||||||
from dikte import settings_ui
|
from dikte import settings_ui
|
||||||
from dikte import update
|
from dikte import update
|
||||||
|
from dikte.i18n import t
|
||||||
from tests.support import DikteTest, only_these_tools
|
from tests.support import DikteTest, only_these_tools
|
||||||
|
|
||||||
# The harness below replaces this method on the class so that opening a window
|
# The harness below replaces this method on the class so that opening a window
|
||||||
@@ -1298,6 +1300,54 @@ class LocalModels(DikteTest):
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
_app.processEvents()
|
_app.processEvents()
|
||||||
self.assertEqual(fetch.call_count, 1)
|
self.assertEqual(fetch.call_count, 1)
|
||||||
|
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_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):
|
def test_only_the_chosen_transcriber_is_on_screen(self):
|
||||||
window = self.window(self.config(transcribe_provider="openai"))
|
window = self.window(self.config(transcribe_provider="openai"))
|
||||||
|
|||||||
Reference in New Issue
Block a user