The program had no licence file at all, and pyproject claimed MIT into the void. It is now the GNU General Public License, version 3 or later: LICENSE holds the text verbatim, pyproject declares it with the OSI classifier, both .deb builds write /usr/share/doc/<pkg>/copyright in the machine-readable format Policy requires, both manuals carry a COPYING section, and --version prints the GNU notice on both programs. INSTALL.md is the step-by-step: what you need, the Debian package, the virtual environment for everywhere else, how to check it worked, every optional dependency with what it buys and what happens without it, and the errors people actually hit first -- PEP 668 at the top, because on Debian a plain "pip install ." refuses and reads as a broken program. Speech transcription gets its own four steps, because it is the only part with a real download in it: the recogniser into the environment bandsaunter runs from, checking it took, the model (base.en, 148 MB, from Hugging Face into ~/.cache/huggingface, fetched deliberately rather than in the middle of a scan), then turning it on. With the sizes of every model, the offline routes, and what to do when --engines says no although pip says yes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
153 lines
6.3 KiB
Bash
Executable file
153 lines
6.3 KiB
Bash
Executable file
#!/bin/sh
|
|
# Build every package and an apt repository to serve them from.
|
|
#
|
|
# Produces three packages:
|
|
# bandsaunter the application itself
|
|
# bandsaunter-transcribe the speech recogniser, which Debian does not
|
|
# package, vendored into a private directory
|
|
# bandsaunter-model-<name> the recogniser's model, so a machine never
|
|
# has to reach the network
|
|
#
|
|
# The result is a flat apt repository: point a machine at it and
|
|
# "apt install bandsaunter" brings the lot, offline from then on.
|
|
set -eu
|
|
|
|
here=$(cd "$(dirname "$0")/.." && pwd)
|
|
out=${1:-$here/dist/repo}
|
|
model=${MODEL:-base.en}
|
|
hf_repo=${HF_REPO:-Systran/faster-whisper-$model}
|
|
revision=${DEB_REVISION:-1}
|
|
version=$(cd "$here" && python3 -c 'import bandsaunter; print(bandsaunter.debian_version())')
|
|
|
|
# Compiled wheels tie this to one architecture. amd64 only, by design.
|
|
arch=amd64
|
|
|
|
mkdir -p "$out"
|
|
stage=$(mktemp -d)
|
|
trap 'rm -rf "$stage"' EXIT
|
|
|
|
say() { printf '%s\n' "$*" >&2; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "building bandsaunter ${version}-${revision}"
|
|
"$here/packaging/build-deb.sh" "$out" >/dev/null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "collecting the speech recogniser (this downloads from PyPI once)"
|
|
wheels="$stage/wheels"
|
|
mkdir -p "$wheels"
|
|
pip download --quiet --only-binary=:all: --dest "$wheels" faster-whisper >&2
|
|
|
|
pkg="$stage/bandsaunter-transcribe_${version}-${revision}_${arch}"
|
|
vendor="$pkg/usr/lib/bandsaunter/vendor"
|
|
mkdir -p "$vendor" "$pkg/DEBIAN" "$pkg/usr/share/doc/bandsaunter-transcribe"
|
|
for w in "$wheels"/*.whl; do
|
|
python3 -m zipfile -e "$w" "$vendor/"
|
|
done
|
|
# Debian already provides these and they win on sys.path anyway, so shipping
|
|
# them would be dead weight.
|
|
rm -rf "$vendor"/numpy "$vendor"/numpy-* "$vendor"/yaml "$vendor"/PyYAML-* \
|
|
"$vendor"/setuptools "$vendor"/setuptools-* "$vendor"/pkg_resources \
|
|
"$vendor"/_yaml "$vendor"/_distutils_hack "$vendor"/numpy.libs
|
|
# A .pth file only runs inside a real site directory; here it is dead weight.
|
|
rm -f "$vendor"/*.pth
|
|
find "$vendor" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
|
|
|
# The packaging is this project's; the wheels inside keep their own terms.
|
|
{
|
|
echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
|
|
echo "Upstream-Name: bandsaunter"
|
|
echo
|
|
echo "Files: *"
|
|
echo "Copyright: 2026 The Dust Council"
|
|
echo "License: GPL-3.0-or-later"
|
|
echo " The packaging in this file is under the GNU General Public License,"
|
|
echo " version 3 or later, whose full text is in the bandsaunter package at"
|
|
echo " /usr/share/doc/bandsaunter/copyright. The Python wheels vendored"
|
|
echo " under /usr/lib/bandsaunter/vendor are third-party works distributed"
|
|
echo " under their own licences, each carried in that package's own"
|
|
echo " dist-info directory beside it."
|
|
} > "$pkg/usr/share/doc/bandsaunter-transcribe/copyright"
|
|
|
|
cat > "$pkg/DEBIAN/control" <<EOF
|
|
Package: bandsaunter-transcribe
|
|
Version: ${version}-${revision}
|
|
Section: hamradio
|
|
Priority: optional
|
|
Architecture: ${arch}
|
|
Depends: bandsaunter (= ${version}-${revision}), python3-numpy, python3-yaml
|
|
Recommends: bandsaunter-model-$(echo "$model" | tr '._' '--')
|
|
Maintainer: bandsaunter
|
|
Installed-Size: $(du -ks "$pkg" | cut -f1)
|
|
Description: speech recogniser for bandsaunter
|
|
Transcribes recorded voice transmissions to text.
|
|
.
|
|
No speech recogniser is packaged for Debian, so faster-whisper and its
|
|
dependencies are installed here into a private directory rather than into
|
|
dist-packages. That directory is searched after the system one, so anything
|
|
apt provides still takes precedence and these copies only fill the gap.
|
|
EOF
|
|
fakeroot dpkg-deb --build -Zxz "$pkg" "$out" >/dev/null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "collecting the $model model"
|
|
modelpkg="$stage/bandsaunter-model-$(echo "$model" | tr '._' '--')_${version}-${revision}_all"
|
|
modeldir="$modelpkg/usr/share/bandsaunter/models/$model"
|
|
mkdir -p "$modeldir" "$modelpkg/DEBIAN"
|
|
modeldoc="$modelpkg/usr/share/doc/bandsaunter-model-$(echo "$model" | tr '._' '--')"
|
|
mkdir -p "$modeldoc"
|
|
PYTHONPATH="$stage/wheels-unused" python3 - "$hf_repo" "$modeldir" <<'PY' >&2
|
|
import shutil, sys
|
|
from pathlib import Path
|
|
from huggingface_hub import snapshot_download
|
|
src = Path(snapshot_download(sys.argv[1]))
|
|
dst = Path(sys.argv[2])
|
|
for item in src.iterdir():
|
|
if item.name.startswith("."):
|
|
continue
|
|
target = dst / item.name
|
|
# Resolve the cache's symlinks so the package holds real files.
|
|
shutil.copy2(item.resolve(), target)
|
|
PY
|
|
|
|
{
|
|
echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
|
|
echo "Upstream-Name: bandsaunter"
|
|
echo
|
|
echo "Files: *"
|
|
echo "Copyright: 2026 The Dust Council"
|
|
echo "License: GPL-3.0-or-later"
|
|
echo " The packaging is under the GNU General Public License, version 3 or"
|
|
echo " later, whose full text is in the bandsaunter package at"
|
|
echo " /usr/share/doc/bandsaunter/copyright. The model weights under"
|
|
echo " /usr/share/bandsaunter/models are a third-party work, redistributed"
|
|
echo " under the terms the model was published with."
|
|
} > "$modeldoc/copyright"
|
|
|
|
cat > "$modelpkg/DEBIAN/control" <<EOF
|
|
Package: bandsaunter-model-$(echo "$model" | tr '._' '--')
|
|
Version: ${version}-${revision}
|
|
Section: hamradio
|
|
Priority: optional
|
|
Architecture: all
|
|
Depends: bandsaunter-transcribe
|
|
Maintainer: bandsaunter
|
|
Installed-Size: $(du -ks "$modelpkg" | cut -f1)
|
|
Description: $model speech model for bandsaunter
|
|
The $model recognition model, installed locally so that transcription works
|
|
without reaching the network. Without this package the recogniser downloads
|
|
the model the first time it is used, on every machine.
|
|
EOF
|
|
fakeroot dpkg-deb --build -Zxz "$modelpkg" "$out" >/dev/null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "indexing the repository"
|
|
( cd "$out" && dpkg-scanpackages --multiversion . /dev/null > Packages 2>/dev/null
|
|
gzip -9kfn Packages
|
|
apt-ftparchive -o APT::FTPArchive::Release::Suite=stable \
|
|
-o APT::FTPArchive::Release::Codename=bandsaunter \
|
|
release . > Release )
|
|
|
|
say ""
|
|
say "repository ready: $out"
|
|
ls -1sh "$out"/*.deb >&2
|